Financial & BankingParse → Extract

Bank Statement Extractor

Extracts account summaries, transactions, and balances from bank statements.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source documentBank Statement 1.pdf

Step-by-step

A bank statement is a periodic financial document issued by a financial institution that records all account activity, including opening and closing balances, deposits, withdrawals, fees, and a detailed transaction history for a specified period. This template takes in Bank Statement and outputs markdown (.md) capturing the statement's full text and layout, and JSON (.json) with structured account and transaction fields including bank details, balance information, fee totals, and individual transaction records per the extraction schema by using Extend's Parse, Extract primitives.

Input
Bank Statement
Compatible document types (full list)
.pdf.docx.xlsx.png.jpg.jpeg.tiff.tif.svg.heic.heif.bmp.gif.webp.psd.xls.xltm.xltx.ods.doc.wpd.dotx.odt.pptx.ppt.ppm.csv.txt.html.xml.rtf.lis.md.eml.pcx
Step 1

Parse

Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.

InputSource document — PDF, image, spreadsheet, presentation, or scan
Config
blockOptions.text.agentic.enabledtruechanged
chunkingStrategy.type"document"
engine"parse_performance"
OutputMarkdown chunked by page or section, plus typed blocks (text, table, figure) with bounding boxes

You can learn more about Parse configuration in Extend's Parse documentation.

Step 2

Extract

Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.

InputOutput of the Parse step
Config
metadata.account_holder_name.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":55.0872,"y":141.012},{"x":126.96480000000001,"y":141.0552},{"x":126.95760000000001,"y":150.7104…changed
metadata.account_holder_name.logprobsConfidencenullchanged
metadata.account_holder_name.ocrConfidence0.992changed
metadata.account_number.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":361.08,"y":60.0192},{"x":422.5824,"y":60.0264},{"x":422.5824,"y":68.3928},{"x":361.08,"y":68.38…changed
metadata.account_number.logprobsConfidencenullchanged
metadata.account_number.ocrConfidence0.992changed
metadata.account_type.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":202.9176,"y":381.0528},{"x":292.9608,"y":381.4056},{"x":292.9104,"y":392.88239999999996},{"x":2…changed
metadata.account_type.logprobsConfidencenullchanged
metadata.account_type.ocrConfidence0.991changed
metadata.bank_name.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":58.2768,"y":54.309599999999996},{"x":157.7736,"y":54.2016},{"x":157.7808,"y":62.906400000000005…changed
metadata.bank_name.logprobsConfidencenullchanged
metadata.bank_name.ocrConfidence0.993changed
metadata.branch_address.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":58.176,"y":63.5616},{"x":115.27199999999999,"y":63.684},{"x":115.2576,"y":71.8776},{"x":58.1543…changed
metadata.branch_address.logprobsConfidencenullchanged
metadata.branch_address.ocrConfidence0.086changed
metadata.closing_balance.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":39.0168,"y":480.2832},{"x":108.4536,"y":480.3768},{"x":108.4392,"y":490.6656},{"x":39.002399999…changed
metadata.closing_balance.logprobsConfidencenullchanged
metadata.closing_balance.ocrConfidence0.995changed
metadata.opening_balance.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":38.9736,"y":413.532},{"x":121.9248,"y":413.5608},{"x":121.9248,"y":424.46880000000004},{"x":38.…changed
metadata.opening_balance.logprobsConfidencenullchanged
metadata.opening_balance.ocrConfidence0.995changed
metadata.statement_issue_date.citations[]changed
metadata.statement_issue_date.logprobsConfidencenullchanged
metadata.statement_period_end_date.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":359.8488,"y":46.360800000000005},{"x":519.7968000000001,"y":46.3824},{"x":519.7968000000001,"y"…changed
metadata.statement_period_end_date.logprobsConfidencenullchanged
metadata.statement_period_end_date.ocrConfidence0.989changed
metadata.statement_period_start_date.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":359.8488,"y":46.360800000000005},{"x":519.7968000000001,"y":46.3824},{"x":519.7968000000001,"y"…changed
metadata.statement_period_start_date.logprobsConfidencenullchanged
metadata.statement_period_start_date.ocrConfidence0.99changed
metadata.total_credits.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":39.1104,"y":426.96},{"x":133.1568,"y":426.9168},{"x":133.164,"y":437.6592},{"x":39.1104,"y":437…changed
metadata.total_credits.logprobsConfidencenullchanged
metadata.total_credits.ocrConfidence0.992changed
metadata.total_debits.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":39.139199999999995,"y":439.7832},{"x":166.1472,"y":439.86240000000004},{"x":166.14000000000001,…changed
metadata.total_debits.logprobsConfidencenullchanged
metadata.total_debits.ocrConfidence0.989changed
metadata.transactions.citations[]changed
metadata.transactions.logprobsConfidencenullchanged
metadata.transactions.ocrConfidence0.83changed
metadata.transactions[0].amount.logprobsConfidencenullchanged
metadata.transactions[0].amount.ocrConfidence0.996changed
metadata.transactions[0].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[0].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[0].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.25920000000001,"y":563.4864},{"x":59.5368,"y":563.508},{"x":59.5224,"y":572.148},{"x":36.244…changed
metadata.transactions[0].description.logprobsConfidencenullchanged
metadata.transactions[0].description.ocrConfidence0.985changed
metadata.transactions[0].logprobsConfidencenullchanged
metadata.transactions[0].ocrConfidence0.985changed
metadata.transactions[0].reference_number.logprobsConfidencenullchanged
metadata.transactions[0].reference_number.ocrConfidence0.995changed
metadata.transactions[0].transaction_date.logprobsConfidencenullchanged
metadata.transactions[0].transaction_date.ocrConfidence0.99changed
metadata.transactions[0].transaction_type.logprobsConfidencenullchanged
metadata.transactions[0].transaction_type.ocrConfidence0.993changed
metadata.transactions[10].amount.logprobsConfidencenullchanged
metadata.transactions[10].amount.ocrConfidence0.995changed
metadata.transactions[10].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[10].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[10].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":35.6616,"y":723.5136},{"x":59.6232,"y":723.5136},{"x":59.6232,"y":731.8368},{"x":35.6616,"y":73…changed
metadata.transactions[10].description.logprobsConfidencenullchanged
metadata.transactions[10].description.ocrConfidence0.988changed
metadata.transactions[10].logprobsConfidencenullchanged
metadata.transactions[10].ocrConfidence0.988changed
metadata.transactions[10].reference_number.citations[]changed
metadata.transactions[10].reference_number.logprobsConfidencenullchanged
metadata.transactions[10].transaction_date.logprobsConfidencenullchanged
metadata.transactions[10].transaction_date.ocrConfidence0.989changed
metadata.transactions[10].transaction_type.logprobsConfidencenullchanged
metadata.transactions[10].transaction_type.ocrConfidence0.993changed
metadata.transactions[11].amount.logprobsConfidencenullchanged
metadata.transactions[11].amount.ocrConfidence0.995changed
metadata.transactions[11].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[11].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[11].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.7176,"y":130.0464},{"x":57.5568,"y":130.1184},{"x":57.5352,"y":139.0032},{"x":33.6888,"y":13…changed
metadata.transactions[11].description.logprobsConfidencenullchanged
metadata.transactions[11].description.ocrConfidence0.985changed
metadata.transactions[11].logprobsConfidencenullchanged
metadata.transactions[11].ocrConfidence0.985changed
metadata.transactions[11].reference_number.citations[]changed
metadata.transactions[11].reference_number.logprobsConfidencenullchanged
metadata.transactions[11].transaction_date.logprobsConfidencenullchanged
metadata.transactions[11].transaction_date.ocrConfidence0.991changed
metadata.transactions[11].transaction_type.logprobsConfidencenullchanged
metadata.transactions[11].transaction_type.ocrConfidence0.992changed
metadata.transactions[12].amount.logprobsConfidencenullchanged
metadata.transactions[12].amount.ocrConfidence0.995changed
metadata.transactions[12].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[12].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[12].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.588,"y":142.4016},{"x":57.5064,"y":142.4448},{"x":57.492,"y":151.3944},{"x":33.5736,"y":151.…changed
metadata.transactions[12].description.logprobsConfidencenullchanged
metadata.transactions[12].description.ocrConfidence0.9changed
metadata.transactions[12].logprobsConfidencenullchanged
metadata.transactions[12].ocrConfidence0.9changed
metadata.transactions[12].reference_number.citations[]changed
metadata.transactions[12].reference_number.logprobsConfidencenullchanged
metadata.transactions[12].transaction_date.logprobsConfidencenullchanged
metadata.transactions[12].transaction_date.ocrConfidence0.991changed
metadata.transactions[12].transaction_type.logprobsConfidencenullchanged
metadata.transactions[12].transaction_type.ocrConfidence0.992changed
metadata.transactions[13].amount.logprobsConfidencenullchanged
metadata.transactions[13].amount.ocrConfidence0.997changed
metadata.transactions[13].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[13].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[13].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6168,"y":154.7424},{"x":57.132,"y":154.74960000000002},{"x":57.132,"y":163.7424},{"x":33.616…changed
metadata.transactions[13].description.logprobsConfidencenullchanged
metadata.transactions[13].description.ocrConfidence0.916changed
metadata.transactions[13].logprobsConfidencenullchanged
metadata.transactions[13].ocrConfidence0.916changed
metadata.transactions[13].reference_number.logprobsConfidencenullchanged
metadata.transactions[13].reference_number.ocrConfidence0.995changed
metadata.transactions[13].transaction_date.logprobsConfidencenullchanged
metadata.transactions[13].transaction_date.ocrConfidence0.991changed
metadata.transactions[13].transaction_type.logprobsConfidencenullchanged
metadata.transactions[13].transaction_type.ocrConfidence0.992changed
metadata.transactions[14].amount.logprobsConfidencenullchanged
metadata.transactions[14].amount.ocrConfidence0.998changed
metadata.transactions[14].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[14].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[14].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6096,"y":167.148},{"x":57.1248,"y":167.1624},{"x":57.1176,"y":176.0616},{"x":33.6024,"y":176…changed
metadata.transactions[14].description.logprobsConfidencenullchanged
metadata.transactions[14].description.ocrConfidence0.964changed
metadata.transactions[14].logprobsConfidencenullchanged
metadata.transactions[14].ocrConfidence0.964changed
metadata.transactions[14].reference_number.logprobsConfidencenullchanged
metadata.transactions[14].reference_number.ocrConfidence0.994changed
metadata.transactions[14].transaction_date.logprobsConfidencenullchanged
metadata.transactions[14].transaction_date.ocrConfidence0.991changed
metadata.transactions[14].transaction_type.logprobsConfidencenullchanged
metadata.transactions[14].transaction_type.ocrConfidence0.992changed
metadata.transactions[15].amount.logprobsConfidencenullchanged
metadata.transactions[15].amount.ocrConfidence0.995changed
metadata.transactions[15].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[15].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[15].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5376,"y":179.0136},{"x":57.088800000000006,"y":179.07840000000002},{"x":57.0672,"y":188.1720…changed
metadata.transactions[15].description.logprobsConfidencenullchanged
metadata.transactions[15].description.ocrConfidence0.881changed
metadata.transactions[15].logprobsConfidencenullchanged
metadata.transactions[15].ocrConfidence0.881changed
metadata.transactions[15].reference_number.citations[]changed
metadata.transactions[15].reference_number.logprobsConfidencenullchanged
metadata.transactions[15].transaction_date.logprobsConfidencenullchanged
metadata.transactions[15].transaction_date.ocrConfidence0.991changed
metadata.transactions[15].transaction_type.logprobsConfidencenullchanged
metadata.transactions[15].transaction_type.ocrConfidence0.992changed
metadata.transactions[16].amount.logprobsConfidencenullchanged
metadata.transactions[16].amount.ocrConfidence0.996changed
metadata.transactions[16].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[16].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[16].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.552,"y":200.49120000000002},{"x":57.002399999999994,"y":200.4984},{"x":57.002399999999994,"y…changed
metadata.transactions[16].description.logprobsConfidencenullchanged
metadata.transactions[16].description.ocrConfidence0.985changed
metadata.transactions[16].logprobsConfidencenullchanged
metadata.transactions[16].ocrConfidence0.985changed
metadata.transactions[16].reference_number.citations[]changed
metadata.transactions[16].reference_number.logprobsConfidencenullchanged
metadata.transactions[16].transaction_date.logprobsConfidencenullchanged
metadata.transactions[16].transaction_date.ocrConfidence0.991changed
metadata.transactions[16].transaction_type.logprobsConfidencenullchanged
metadata.transactions[16].transaction_type.ocrConfidence0.992changed
metadata.transactions[17].amount.logprobsConfidencenullchanged
metadata.transactions[17].amount.ocrConfidence0.997changed
metadata.transactions[17].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[17].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[17].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6744,"y":212.88240000000002},{"x":56.937599999999996,"y":212.8896},{"x":56.937599999999996,"…changed
metadata.transactions[17].description.logprobsConfidencenullchanged
metadata.transactions[17].description.ocrConfidence0.964changed
metadata.transactions[17].logprobsConfidencenullchanged
metadata.transactions[17].ocrConfidence0.964changed
metadata.transactions[17].reference_number.logprobsConfidencenullchanged
metadata.transactions[17].reference_number.ocrConfidence0.995changed
metadata.transactions[17].transaction_date.logprobsConfidencenullchanged
metadata.transactions[17].transaction_date.ocrConfidence0.991changed
metadata.transactions[17].transaction_type.logprobsConfidencenullchanged
metadata.transactions[17].transaction_type.ocrConfidence0.992changed
metadata.transactions[18].amount.logprobsConfidencenullchanged
metadata.transactions[18].amount.ocrConfidence0.995changed
metadata.transactions[18].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[18].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[18].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.580799999999996,"y":225.3672},{"x":55.872,"y":225.37439999999998},{"x":55.872,"y":234.1944},…changed
metadata.transactions[18].description.logprobsConfidencenullchanged
metadata.transactions[18].description.ocrConfidence0.985changed
metadata.transactions[18].logprobsConfidencenullchanged
metadata.transactions[18].ocrConfidence0.985changed
metadata.transactions[18].reference_number.citations[]changed
metadata.transactions[18].reference_number.logprobsConfidencenullchanged
metadata.transactions[18].transaction_date.logprobsConfidencenullchanged
metadata.transactions[18].transaction_date.ocrConfidence0.991changed
metadata.transactions[18].transaction_type.logprobsConfidencenullchanged
metadata.transactions[18].transaction_type.ocrConfidence0.992changed
metadata.transactions[19].amount.logprobsConfidencenullchanged
metadata.transactions[19].amount.ocrConfidence0.996changed
metadata.transactions[19].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[19].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[19].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4368,"y":246.8016},{"x":55.9584,"y":246.8376},{"x":55.944,"y":255.7728},{"x":33.4224,"y":255…changed
metadata.transactions[19].description.logprobsConfidencenullchanged
metadata.transactions[19].description.ocrConfidence0.975changed
metadata.transactions[19].logprobsConfidencenullchanged
metadata.transactions[19].ocrConfidence0.975changed
metadata.transactions[19].reference_number.citations[]changed
metadata.transactions[19].reference_number.logprobsConfidencenullchanged
metadata.transactions[19].transaction_date.logprobsConfidencenullchanged
metadata.transactions[19].transaction_date.ocrConfidence0.991changed
metadata.transactions[19].transaction_type.logprobsConfidencenullchanged
metadata.transactions[19].transaction_type.ocrConfidence0.992changed
metadata.transactions[1].amount.logprobsConfidencenullchanged
metadata.transactions[1].amount.ocrConfidence0.995changed
metadata.transactions[1].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[1].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[1].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.403200000000005,"y":575.9136},{"x":59.0976,"y":576.0144},{"x":59.0616,"y":584.496},{"x":36.3…changed
metadata.transactions[1].description.logprobsConfidencenullchanged
metadata.transactions[1].description.ocrConfidence0.989changed
metadata.transactions[1].logprobsConfidencenullchanged
metadata.transactions[1].ocrConfidence0.989changed
metadata.transactions[1].reference_number.logprobsConfidencenullchanged
metadata.transactions[1].reference_number.ocrConfidence0.994changed
metadata.transactions[1].transaction_date.logprobsConfidencenullchanged
metadata.transactions[1].transaction_date.ocrConfidence0.99changed
metadata.transactions[1].transaction_type.logprobsConfidencenullchanged
metadata.transactions[1].transaction_type.ocrConfidence0.993changed
metadata.transactions[20].amount.logprobsConfidencenullchanged
metadata.transactions[20].amount.ocrConfidence0.997changed
metadata.transactions[20].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[20].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[20].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6528,"y":268.4088},{"x":57.139199999999995,"y":268.4592},{"x":57.1176,"y":277.416},{"x":33.6…changed
metadata.transactions[20].description.logprobsConfidencenullchanged
metadata.transactions[20].description.ocrConfidence0.956changed
metadata.transactions[20].logprobsConfidencenullchanged
metadata.transactions[20].ocrConfidence0.956changed
metadata.transactions[20].reference_number.logprobsConfidencenullchanged
metadata.transactions[20].reference_number.ocrConfidence0.993changed
metadata.transactions[20].transaction_date.logprobsConfidencenullchanged
metadata.transactions[20].transaction_date.ocrConfidence0.991changed
metadata.transactions[20].transaction_type.logprobsConfidencenullchanged
metadata.transactions[20].transaction_type.ocrConfidence0.992changed
metadata.transactions[21].amount.logprobsConfidencenullchanged
metadata.transactions[21].amount.ocrConfidence0.995changed
metadata.transactions[21].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[21].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[21].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.696000000000005,"y":280.7136},{"x":57.132,"y":280.84319999999997},{"x":57.081599999999995,"y…changed
metadata.transactions[21].description.logprobsConfidencenullchanged
metadata.transactions[21].description.ocrConfidence0.964changed
metadata.transactions[21].logprobsConfidencenullchanged
metadata.transactions[21].ocrConfidence0.964changed
metadata.transactions[21].reference_number.logprobsConfidencenullchanged
metadata.transactions[21].reference_number.ocrConfidence0.994changed
metadata.transactions[21].transaction_date.logprobsConfidencenullchanged
metadata.transactions[21].transaction_date.ocrConfidence0.991changed
metadata.transactions[21].transaction_type.logprobsConfidencenullchanged
metadata.transactions[21].transaction_type.ocrConfidence0.992changed
metadata.transactions[22].amount.logprobsConfidencenullchanged
metadata.transactions[22].amount.ocrConfidence0.987changed
metadata.transactions[22].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[22].balance_after_transaction.ocrConfidence0.989changed
metadata.transactions[22].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.7248,"y":293.09040000000005},{"x":57.1176,"y":293.1768},{"x":57.088800000000006,"y":302.0616…changed
metadata.transactions[22].description.logprobsConfidencenullchanged
metadata.transactions[22].description.ocrConfidence0.916changed
metadata.transactions[22].logprobsConfidencenullchanged
metadata.transactions[22].ocrConfidence0.916changed
metadata.transactions[22].reference_number.logprobsConfidencenullchanged
metadata.transactions[22].reference_number.ocrConfidence0.995changed
metadata.transactions[22].transaction_date.logprobsConfidencenullchanged
metadata.transactions[22].transaction_date.ocrConfidence0.991changed
metadata.transactions[22].transaction_type.logprobsConfidencenullchanged
metadata.transactions[22].transaction_type.ocrConfidence0.992changed
metadata.transactions[23].amount.logprobsConfidencenullchanged
metadata.transactions[23].amount.ocrConfidence0.995changed
metadata.transactions[23].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[23].balance_after_transaction.ocrConfidence0.994changed
metadata.transactions[23].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.7032,"y":304.99920000000003},{"x":57.0312,"y":305.0712},{"x":57.002399999999994,"y":314.0784…changed
metadata.transactions[23].description.logprobsConfidencenullchanged
metadata.transactions[23].description.ocrConfidence0.916changed
metadata.transactions[23].logprobsConfidencenullchanged
metadata.transactions[23].ocrConfidence0.916changed
metadata.transactions[23].reference_number.citations[]changed
metadata.transactions[23].reference_number.logprobsConfidencenullchanged
metadata.transactions[23].transaction_date.logprobsConfidencenullchanged
metadata.transactions[23].transaction_date.ocrConfidence0.991changed
metadata.transactions[23].transaction_type.logprobsConfidencenullchanged
metadata.transactions[23].transaction_type.ocrConfidence0.992changed
metadata.transactions[24].amount.logprobsConfidencenullchanged
metadata.transactions[24].amount.ocrConfidence0.996changed
metadata.transactions[24].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[24].balance_after_transaction.ocrConfidence0.992changed
metadata.transactions[24].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.732,"y":317.34},{"x":57.06,"y":317.3544},{"x":57.0528,"y":326.41200000000003},{"x":33.7248,"…changed
metadata.transactions[24].description.logprobsConfidencenullchanged
metadata.transactions[24].description.ocrConfidence0.916changed
metadata.transactions[24].logprobsConfidencenullchanged
metadata.transactions[24].ocrConfidence0.916changed
metadata.transactions[24].reference_number.citations[]changed
metadata.transactions[24].reference_number.logprobsConfidencenullchanged
metadata.transactions[24].transaction_date.logprobsConfidencenullchanged
metadata.transactions[24].transaction_date.ocrConfidence0.991changed
metadata.transactions[24].transaction_type.logprobsConfidencenullchanged
metadata.transactions[24].transaction_type.ocrConfidence0.992changed
metadata.transactions[25].amount.logprobsConfidencenullchanged
metadata.transactions[25].amount.ocrConfidence0.996changed
metadata.transactions[25].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[25].balance_after_transaction.ocrConfidence0.967changed
metadata.transactions[25].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6024,"y":329.61600000000004},{"x":57.3264,"y":329.688},{"x":57.297599999999996,"y":338.88239…changed
metadata.transactions[25].description.logprobsConfidencenullchanged
metadata.transactions[25].description.ocrConfidence0.959changed
metadata.transactions[25].logprobsConfidencenullchanged
metadata.transactions[25].ocrConfidence0.959changed
metadata.transactions[25].reference_number.logprobsConfidencenullchanged
metadata.transactions[25].reference_number.ocrConfidence0.992changed
metadata.transactions[25].transaction_date.logprobsConfidencenullchanged
metadata.transactions[25].transaction_date.ocrConfidence0.991changed
metadata.transactions[25].transaction_type.logprobsConfidencenullchanged
metadata.transactions[25].transaction_type.ocrConfidence0.992changed
metadata.transactions[26].amount.logprobsConfidencenullchanged
metadata.transactions[26].amount.ocrConfidence0.995changed
metadata.transactions[26].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[26].balance_after_transaction.ocrConfidence0.992changed
metadata.transactions[26].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.544799999999995,"y":379.08720000000005},{"x":57.4344,"y":379.1088},{"x":57.42,"y":388.116000…changed
metadata.transactions[26].description.logprobsConfidencenullchanged
metadata.transactions[26].description.ocrConfidence0.984changed
metadata.transactions[26].logprobsConfidencenullchanged
metadata.transactions[26].ocrConfidence0.984changed
metadata.transactions[26].reference_number.citations[]changed
metadata.transactions[26].reference_number.logprobsConfidencenullchanged
metadata.transactions[26].transaction_date.logprobsConfidencenullchanged
metadata.transactions[26].transaction_date.ocrConfidence0.991changed
metadata.transactions[26].transaction_type.logprobsConfidencenullchanged
metadata.transactions[26].transaction_type.ocrConfidence0.992changed
metadata.transactions[27].amount.logprobsConfidencenullchanged
metadata.transactions[27].amount.ocrConfidence0.995changed
metadata.transactions[27].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[27].balance_after_transaction.ocrConfidence0.992changed
metadata.transactions[27].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5376,"y":400.92479999999995},{"x":57.4632,"y":400.9392},{"x":57.456,"y":410.09040000000005},…changed
metadata.transactions[27].description.logprobsConfidencenullchanged
metadata.transactions[27].description.ocrConfidence0.881changed
metadata.transactions[27].logprobsConfidencenullchanged
metadata.transactions[27].ocrConfidence0.881changed
metadata.transactions[27].reference_number.citations[]changed
metadata.transactions[27].reference_number.logprobsConfidencenullchanged
metadata.transactions[27].transaction_date.logprobsConfidencenullchanged
metadata.transactions[27].transaction_date.ocrConfidence0.991changed
metadata.transactions[27].transaction_type.logprobsConfidencenullchanged
metadata.transactions[27].transaction_type.ocrConfidence0.992changed
metadata.transactions[28].amount.logprobsConfidencenullchanged
metadata.transactions[28].amount.ocrConfidence0.995changed
metadata.transactions[28].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[28].balance_after_transaction.ocrConfidence0.992changed
metadata.transactions[28].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.3576,"y":422.5608},{"x":57.355199999999996,"y":422.48879999999997},{"x":57.3768,"y":431.7335…changed
metadata.transactions[28].description.logprobsConfidencenullchanged
metadata.transactions[28].description.ocrConfidence0.881changed
metadata.transactions[28].logprobsConfidencenullchanged
metadata.transactions[28].ocrConfidence0.881changed
metadata.transactions[28].reference_number.citations[]changed
metadata.transactions[28].reference_number.logprobsConfidencenullchanged
metadata.transactions[28].transaction_date.logprobsConfidencenullchanged
metadata.transactions[28].transaction_date.ocrConfidence0.991changed
metadata.transactions[28].transaction_type.logprobsConfidencenullchanged
metadata.transactions[28].transaction_type.ocrConfidence0.992changed
metadata.transactions[29].amount.logprobsConfidencenullchanged
metadata.transactions[29].amount.ocrConfidence0.995changed
metadata.transactions[29].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[29].balance_after_transaction.ocrConfidence0.992changed
metadata.transactions[29].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4728,"y":443.9448},{"x":57.448800000000006,"y":443.8872},{"x":57.4704,"y":453.22560000000004…changed
metadata.transactions[29].description.logprobsConfidencenullchanged
metadata.transactions[29].description.ocrConfidence0.83changed
metadata.transactions[29].logprobsConfidencenullchanged
metadata.transactions[29].ocrConfidence0.83changed
metadata.transactions[29].reference_number.logprobsConfidencenullchanged
metadata.transactions[29].reference_number.ocrConfidence0.99changed
metadata.transactions[29].transaction_date.logprobsConfidencenullchanged
metadata.transactions[29].transaction_date.ocrConfidence0.991changed
metadata.transactions[29].transaction_type.logprobsConfidencenullchanged
metadata.transactions[29].transaction_type.ocrConfidence0.992changed
metadata.transactions[2].amount.logprobsConfidencenullchanged
metadata.transactions[2].amount.ocrConfidence0.995changed
metadata.transactions[2].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[2].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[2].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.1512,"y":588.132},{"x":59.0976,"y":588.1536},{"x":59.0904,"y":596.8008},{"x":36.1368,"y":596…changed
metadata.transactions[2].description.logprobsConfidencenullchanged
metadata.transactions[2].description.ocrConfidence0.977changed
metadata.transactions[2].logprobsConfidencenullchanged
metadata.transactions[2].ocrConfidence0.977changed
metadata.transactions[2].reference_number.citations[]changed
metadata.transactions[2].reference_number.logprobsConfidencenullchanged
metadata.transactions[2].transaction_date.logprobsConfidencenullchanged
metadata.transactions[2].transaction_date.ocrConfidence0.99changed
metadata.transactions[2].transaction_type.logprobsConfidencenullchanged
metadata.transactions[2].transaction_type.ocrConfidence0.993changed
metadata.transactions[30].amount.logprobsConfidencenullchanged
metadata.transactions[30].amount.ocrConfidence0.994changed
metadata.transactions[30].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[30].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[30].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.588,"y":475.2},{"x":57.355199999999996,"y":475.2},{"x":57.355199999999996,"y":484.3008},{"x"…changed
metadata.transactions[30].description.logprobsConfidencenullchanged
metadata.transactions[30].description.ocrConfidence0.964changed
metadata.transactions[30].logprobsConfidencenullchanged
metadata.transactions[30].ocrConfidence0.964changed
metadata.transactions[30].reference_number.logprobsConfidencenullchanged
metadata.transactions[30].reference_number.ocrConfidence0.995changed
metadata.transactions[30].transaction_date.logprobsConfidencenullchanged
metadata.transactions[30].transaction_date.ocrConfidence0.991changed
metadata.transactions[30].transaction_type.logprobsConfidencenullchanged
metadata.transactions[30].transaction_type.ocrConfidence0.992changed
metadata.transactions[31].amount.logprobsConfidencenullchanged
metadata.transactions[31].amount.ocrConfidence0.995changed
metadata.transactions[31].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[31].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[31].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6456,"y":487.10159999999996},{"x":57.2616,"y":487.1592},{"x":57.232800000000005,"y":496.1952…changed
metadata.transactions[31].description.logprobsConfidencenullchanged
metadata.transactions[31].description.ocrConfidence0.982changed
metadata.transactions[31].logprobsConfidencenullchanged
metadata.transactions[31].ocrConfidence0.982changed
metadata.transactions[31].reference_number.citations[]changed
metadata.transactions[31].reference_number.logprobsConfidencenullchanged
metadata.transactions[31].transaction_date.logprobsConfidencenullchanged
metadata.transactions[31].transaction_date.ocrConfidence0.991changed
metadata.transactions[31].transaction_type.logprobsConfidencenullchanged
metadata.transactions[31].transaction_type.ocrConfidence0.998changed
metadata.transactions[32].amount.logprobsConfidencenullchanged
metadata.transactions[32].amount.ocrConfidence0.993changed
metadata.transactions[32].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[32].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[32].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4584,"y":499.41360000000003},{"x":57.0672,"y":499.4064},{"x":57.0672,"y":508.6944},{"x":33.4…changed
metadata.transactions[32].description.logprobsConfidencenullchanged
metadata.transactions[32].description.ocrConfidence0.881changed
metadata.transactions[32].logprobsConfidencenullchanged
metadata.transactions[32].ocrConfidence0.881changed
metadata.transactions[32].reference_number.citations[]changed
metadata.transactions[32].reference_number.logprobsConfidencenullchanged
metadata.transactions[32].transaction_date.logprobsConfidencenullchanged
metadata.transactions[32].transaction_date.ocrConfidence0.991changed
metadata.transactions[32].transaction_type.logprobsConfidencenullchanged
metadata.transactions[32].transaction_type.ocrConfidence0.992changed
metadata.transactions[33].amount.logprobsConfidencenullchanged
metadata.transactions[33].amount.ocrConfidence0.992changed
metadata.transactions[33].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[33].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[33].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4584,"y":520.8912},{"x":57.132,"y":520.9056},{"x":57.1248,"y":530.0568000000001},{"x":33.451…changed
metadata.transactions[33].description.logprobsConfidencenullchanged
metadata.transactions[33].description.ocrConfidence0.977changed
metadata.transactions[33].logprobsConfidencenullchanged
metadata.transactions[33].ocrConfidence0.977changed
metadata.transactions[33].reference_number.citations[]changed
metadata.transactions[33].reference_number.logprobsConfidencenullchanged
metadata.transactions[33].transaction_date.logprobsConfidencenullchanged
metadata.transactions[33].transaction_date.ocrConfidence0.991changed
metadata.transactions[33].transaction_type.logprobsConfidencenullchanged
metadata.transactions[33].transaction_type.ocrConfidence0.992changed
metadata.transactions[34].amount.logprobsConfidencenullchanged
metadata.transactions[34].amount.ocrConfidence0.996changed
metadata.transactions[34].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[34].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[34].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.544799999999995,"y":542.484},{"x":56.9088,"y":542.556},{"x":56.88,"y":551.6496000000001},{"x…changed
metadata.transactions[34].description.logprobsConfidencenullchanged
metadata.transactions[34].description.ocrConfidence0.985changed
metadata.transactions[34].logprobsConfidencenullchanged
metadata.transactions[34].ocrConfidence0.985changed
metadata.transactions[34].reference_number.citations[]changed
metadata.transactions[34].reference_number.logprobsConfidencenullchanged
metadata.transactions[34].transaction_date.logprobsConfidencenullchanged
metadata.transactions[34].transaction_date.ocrConfidence0.991changed
metadata.transactions[34].transaction_type.logprobsConfidencenullchanged
metadata.transactions[34].transaction_type.ocrConfidence0.992changed
metadata.transactions[35].amount.logprobsConfidencenullchanged
metadata.transactions[35].amount.ocrConfidence0.997changed
metadata.transactions[35].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[35].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[35].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5736,"y":554.7528},{"x":56.9592,"y":554.8464},{"x":56.923199999999994,"y":563.9976},{"x":33.…changed
metadata.transactions[35].description.logprobsConfidencenullchanged
metadata.transactions[35].description.ocrConfidence0.985changed
metadata.transactions[35].logprobsConfidencenullchanged
metadata.transactions[35].ocrConfidence0.985changed
metadata.transactions[35].reference_number.citations[]changed
metadata.transactions[35].reference_number.logprobsConfidencenullchanged
metadata.transactions[35].transaction_date.logprobsConfidencenullchanged
metadata.transactions[35].transaction_date.ocrConfidence0.991changed
metadata.transactions[35].transaction_type.logprobsConfidencenullchanged
metadata.transactions[35].transaction_type.ocrConfidence0.992changed
metadata.transactions[36].amount.logprobsConfidencenullchanged
metadata.transactions[36].amount.ocrConfidence0.995changed
metadata.transactions[36].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[36].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[36].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6168,"y":567.288},{"x":56.8224,"y":567.3672},{"x":56.7936,"y":576.2520000000001},{"x":33.588…changed
metadata.transactions[36].description.logprobsConfidencenullchanged
metadata.transactions[36].description.ocrConfidence0.985changed
metadata.transactions[36].logprobsConfidencenullchanged
metadata.transactions[36].ocrConfidence0.985changed
metadata.transactions[36].reference_number.citations[]changed
metadata.transactions[36].reference_number.logprobsConfidencenullchanged
metadata.transactions[36].transaction_date.logprobsConfidencenullchanged
metadata.transactions[36].transaction_date.ocrConfidence0.991changed
metadata.transactions[36].transaction_type.logprobsConfidencenullchanged
metadata.transactions[36].transaction_type.ocrConfidence0.992changed
metadata.transactions[37].amount.logprobsConfidencenullchanged
metadata.transactions[37].amount.ocrConfidence0.996changed
metadata.transactions[37].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[37].balance_after_transaction.ocrConfidence0.999changed
metadata.transactions[37].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.552,"y":579.2615999999999},{"x":55.98,"y":579.3624},{"x":55.944,"y":588.2760000000001},{"x":…changed
metadata.transactions[37].description.logprobsConfidencenullchanged
metadata.transactions[37].description.ocrConfidence0.837changed
metadata.transactions[37].logprobsConfidencenullchanged
metadata.transactions[37].ocrConfidence0.837changed
metadata.transactions[37].reference_number.citations[]changed
metadata.transactions[37].reference_number.logprobsConfidencenullchanged
metadata.transactions[37].transaction_date.logprobsConfidencenullchanged
metadata.transactions[37].transaction_date.ocrConfidence0.991changed
metadata.transactions[37].transaction_type.logprobsConfidencenullchanged
metadata.transactions[37].transaction_type.ocrConfidence0.992changed
metadata.transactions[38].amount.logprobsConfidencenullchanged
metadata.transactions[38].amount.ocrConfidence0.997changed
metadata.transactions[38].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[38].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[38].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.588,"y":591.6168},{"x":57.1248,"y":591.7031999999999},{"x":57.088800000000006,"y":600.6384},…changed
metadata.transactions[38].description.logprobsConfidencenullchanged
metadata.transactions[38].description.ocrConfidence0.964changed
metadata.transactions[38].logprobsConfidencenullchanged
metadata.transactions[38].ocrConfidence0.964changed
metadata.transactions[38].reference_number.logprobsConfidencenullchanged
metadata.transactions[38].reference_number.ocrConfidence0.994changed
metadata.transactions[38].transaction_date.logprobsConfidencenullchanged
metadata.transactions[38].transaction_date.ocrConfidence0.991changed
metadata.transactions[38].transaction_type.logprobsConfidencenullchanged
metadata.transactions[38].transaction_type.ocrConfidence0.992changed
metadata.transactions[39].amount.logprobsConfidencenullchanged
metadata.transactions[39].amount.ocrConfidence0.995changed
metadata.transactions[39].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[39].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[39].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6024,"y":603.9288},{"x":57.002399999999994,"y":603.972},{"x":56.988,"y":612.9864},{"x":33.58…changed
metadata.transactions[39].description.logprobsConfidencenullchanged
metadata.transactions[39].description.ocrConfidence0.964changed
metadata.transactions[39].logprobsConfidencenullchanged
metadata.transactions[39].ocrConfidence0.964changed
metadata.transactions[39].reference_number.logprobsConfidencenullchanged
metadata.transactions[39].reference_number.ocrConfidence0.994changed
metadata.transactions[39].transaction_date.logprobsConfidencenullchanged
metadata.transactions[39].transaction_date.ocrConfidence0.991changed
metadata.transactions[39].transaction_type.logprobsConfidencenullchanged
metadata.transactions[39].transaction_type.ocrConfidence0.992changed
metadata.transactions[3].amount.logprobsConfidencenullchanged
metadata.transactions[3].amount.ocrConfidence0.998changed
metadata.transactions[3].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[3].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[3].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.2736,"y":609.5015999999999},{"x":59.169599999999996,"y":609.6456},{"x":59.119200000000006,"y…changed
metadata.transactions[3].description.logprobsConfidencenullchanged
metadata.transactions[3].description.ocrConfidence0.988changed
metadata.transactions[3].logprobsConfidencenullchanged
metadata.transactions[3].ocrConfidence0.988changed
metadata.transactions[3].reference_number.citations[]changed
metadata.transactions[3].reference_number.logprobsConfidencenullchanged
metadata.transactions[3].transaction_date.logprobsConfidencenullchanged
metadata.transactions[3].transaction_date.ocrConfidence0.99changed
metadata.transactions[3].transaction_type.logprobsConfidencenullchanged
metadata.transactions[3].transaction_type.ocrConfidence0.992changed
metadata.transactions[40].amount.logprobsConfidencenullchanged
metadata.transactions[40].amount.ocrConfidence0.994changed
metadata.transactions[40].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[40].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[40].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6168,"y":616.2983999999999},{"x":57.0384,"y":616.356},{"x":57.0168,"y":625.3704},{"x":33.595…changed
metadata.transactions[40].description.logprobsConfidencenullchanged
metadata.transactions[40].description.ocrConfidence0.985changed
metadata.transactions[40].logprobsConfidencenullchanged
metadata.transactions[40].ocrConfidence0.985changed
metadata.transactions[40].reference_number.citations[]changed
metadata.transactions[40].reference_number.logprobsConfidencenullchanged
metadata.transactions[40].transaction_date.logprobsConfidencenullchanged
metadata.transactions[40].transaction_date.ocrConfidence0.991changed
metadata.transactions[40].transaction_type.logprobsConfidencenullchanged
metadata.transactions[40].transaction_type.ocrConfidence0.992changed
metadata.transactions[41].amount.logprobsConfidencenullchanged
metadata.transactions[41].amount.ocrConfidence0.995changed
metadata.transactions[41].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[41].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[41].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6024,"y":628.3223999999999},{"x":57.0096,"y":628.4232},{"x":56.9664,"y":637.3296},{"x":33.56…changed
metadata.transactions[41].description.logprobsConfidencenullchanged
metadata.transactions[41].description.ocrConfidence0.989changed
metadata.transactions[41].logprobsConfidencenullchanged
metadata.transactions[41].ocrConfidence0.989changed
metadata.transactions[41].reference_number.logprobsConfidencenullchanged
metadata.transactions[41].reference_number.ocrConfidence0.992changed
metadata.transactions[41].transaction_date.logprobsConfidencenullchanged
metadata.transactions[41].transaction_date.ocrConfidence0.991changed
metadata.transactions[41].transaction_type.logprobsConfidencenullchanged
metadata.transactions[41].transaction_type.ocrConfidence0.992changed
metadata.transactions[42].amount.logprobsConfidencenullchanged
metadata.transactions[42].amount.ocrConfidence0.995changed
metadata.transactions[42].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[42].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[42].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5376,"y":640.6200000000001},{"x":57.218399999999995,"y":640.6344},{"x":57.2112,"y":649.5336}…changed
metadata.transactions[42].description.logprobsConfidencenullchanged
metadata.transactions[42].description.ocrConfidence0.973changed
metadata.transactions[42].logprobsConfidencenullchanged
metadata.transactions[42].ocrConfidence0.973changed
metadata.transactions[42].reference_number.citations[]changed
metadata.transactions[42].reference_number.logprobsConfidencenullchanged
metadata.transactions[42].transaction_date.logprobsConfidencenullchanged
metadata.transactions[42].transaction_date.ocrConfidence0.991changed
metadata.transactions[42].transaction_type.logprobsConfidencenullchanged
metadata.transactions[42].transaction_type.ocrConfidence0.992changed
metadata.transactions[4].amount.logprobsConfidencenullchanged
metadata.transactions[4].amount.ocrConfidence0.995changed
metadata.transactions[4].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[4].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[4].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.3528,"y":621.8568},{"x":59.0976,"y":621.9504},{"x":59.0616,"y":630.7343999999999},{"x":36.31…changed
metadata.transactions[4].description.logprobsConfidencenullchanged
metadata.transactions[4].description.ocrConfidence0.988changed
metadata.transactions[4].logprobsConfidencenullchanged
metadata.transactions[4].ocrConfidence0.988changed
metadata.transactions[4].reference_number.citations[]changed
metadata.transactions[4].reference_number.logprobsConfidencenullchanged
metadata.transactions[4].transaction_date.logprobsConfidencenullchanged
metadata.transactions[4].transaction_date.ocrConfidence0.99changed
metadata.transactions[4].transaction_type.logprobsConfidencenullchanged
metadata.transactions[4].transaction_type.ocrConfidence0.993changed
metadata.transactions[5].amount.logprobsConfidencenullchanged
metadata.transactions[5].amount.ocrConfidence0.996changed
metadata.transactions[5].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[5].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[5].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.3744,"y":633.9888000000001},{"x":59.0112,"y":634.1256},{"x":58.9608,"y":642.8232},{"x":36.31…changed
metadata.transactions[5].description.logprobsConfidencenullchanged
metadata.transactions[5].description.ocrConfidence0.988changed
metadata.transactions[5].logprobsConfidencenullchanged
metadata.transactions[5].ocrConfidence0.988changed
metadata.transactions[5].reference_number.citations[]changed
metadata.transactions[5].reference_number.logprobsConfidencenullchanged
metadata.transactions[5].transaction_date.logprobsConfidencenullchanged
metadata.transactions[5].transaction_date.ocrConfidence0.99changed
metadata.transactions[5].transaction_type.logprobsConfidencenullchanged
metadata.transactions[5].transaction_type.ocrConfidence0.993changed
metadata.transactions[6].amount.logprobsConfidencenullchanged
metadata.transactions[6].amount.ocrConfidence0.995changed
metadata.transactions[6].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[6].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[6].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.36,"y":646.3584},{"x":58.953599999999994,"y":646.4448},{"x":58.9176,"y":654.9552},{"x":36.33…changed
metadata.transactions[6].description.logprobsConfidencenullchanged
metadata.transactions[6].description.ocrConfidence0.977changed
metadata.transactions[6].logprobsConfidencenullchanged
metadata.transactions[6].ocrConfidence0.977changed
metadata.transactions[6].reference_number.citations[]changed
metadata.transactions[6].reference_number.logprobsConfidencenullchanged
metadata.transactions[6].transaction_date.logprobsConfidencenullchanged
metadata.transactions[6].transaction_date.ocrConfidence0.99changed
metadata.transactions[6].transaction_type.logprobsConfidencenullchanged
metadata.transactions[6].transaction_type.ocrConfidence0.993changed
metadata.transactions[7].amount.logprobsConfidencenullchanged
metadata.transactions[7].amount.ocrConfidence0.995changed
metadata.transactions[7].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[7].balance_after_transaction.ocrConfidence0.977changed
metadata.transactions[7].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.230399999999996,"y":667.9943999999999},{"x":59.0688,"y":667.9584},{"x":59.076,"y":676.5264},…changed
metadata.transactions[7].description.logprobsConfidencenullchanged
metadata.transactions[7].description.ocrConfidence0.977changed
metadata.transactions[7].logprobsConfidencenullchanged
metadata.transactions[7].ocrConfidence0.977changed
metadata.transactions[7].reference_number.citations[]changed
metadata.transactions[7].reference_number.logprobsConfidencenullchanged
metadata.transactions[7].transaction_date.logprobsConfidencenullchanged
metadata.transactions[7].transaction_date.ocrConfidence0.99changed
metadata.transactions[7].transaction_type.logprobsConfidencenullchanged
metadata.transactions[7].transaction_type.ocrConfidence0.993changed
metadata.transactions[8].amount.logprobsConfidencenullchanged
metadata.transactions[8].amount.ocrConfidence0.992changed
metadata.transactions[8].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[8].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[8].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.1296,"y":689.4144},{"x":57.9096,"y":689.364},{"x":57.9312,"y":698.0616},{"x":36.1512,"y":698…changed
metadata.transactions[8].description.logprobsConfidencenullchanged
metadata.transactions[8].description.ocrConfidence0.931changed
metadata.transactions[8].logprobsConfidencenullchanged
metadata.transactions[8].ocrConfidence0.931changed
metadata.transactions[8].reference_number.citations[]changed
metadata.transactions[8].reference_number.logprobsConfidencenullchanged
metadata.transactions[8].transaction_date.logprobsConfidencenullchanged
metadata.transactions[8].transaction_date.ocrConfidence0.99changed
metadata.transactions[8].transaction_type.logprobsConfidencenullchanged
metadata.transactions[8].transaction_type.ocrConfidence0.993changed
metadata.transactions[9].amount.logprobsConfidencenullchanged
metadata.transactions[9].amount.ocrConfidence0.995changed
metadata.transactions[9].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[9].balance_after_transaction.ocrConfidence0.994changed
metadata.transactions[9].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":35.712,"y":711.2447999999999},{"x":59.5224,"y":711.216},{"x":59.5296,"y":719.4744},{"x":35.7192…changed
metadata.transactions[9].description.logprobsConfidencenullchanged
metadata.transactions[9].description.ocrConfidence0.988changed
metadata.transactions[9].logprobsConfidencenullchanged
metadata.transactions[9].ocrConfidence0.988changed
metadata.transactions[9].reference_number.citations[]changed
metadata.transactions[9].reference_number.logprobsConfidencenullchanged
metadata.transactions[9].transaction_date.logprobsConfidencenullchanged
metadata.transactions[9].transaction_date.ocrConfidence0.989changed
metadata.transactions[9].transaction_type.logprobsConfidencenullchanged
metadata.transactions[9].transaction_type.ocrConfidence0.993changed
value.account_holder_name"ANGIE M LOPEZ OR JUAN L LOPEZ"changed
value.account_number"000000861006133"changed
value.account_type"High School Checking"changed
value.bank_name"JPMorgan Chase Bank, N.A."changed
value.branch_address"P O Box 182051, Columbus, OH 43218-2051"changed
value.closing_balance.amount459.25changed
value.closing_balance.iso_4217_currency_code"USD"changed
value.opening_balance.amount438.59changed
value.opening_balance.iso_4217_currency_code"USD"changed
value.statement_issue_datenullchanged
value.statement_period_end_date"2021-01-25"changed
value.statement_period_start_date"2020-12-22"changed
value.total_credits.amount3098.99changed
value.total_credits.iso_4217_currency_code"USD"changed
value.total_debits.amount-3078.33changed
value.total_debits.iso_4217_currency_code"USD"changed
value.transactions[{"amount":{"amount":-27.9,"iso_4217_currency_code":"USD"},"description":"Discover E-Payment 6930 Web ID: 2510020270","reference_number":"2510020270","transacti…changed
OutputJSON shaped to the extraction schema, with per-field confidence scores and citations grounding each value to its source location

You can learn more about Extract configuration in Extend's Extract documentation.

Example code

{
  "name": "Bank Statement Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT"
    }
  ]
}
# Bank Statement Processing — Extend AI Skill

## What this pipeline does

This pipeline processes bank statements to extract structured financial data: account details (holder name, number, type, bank name), opening/closing balances, statement period dates, and a complete transaction ledger with individual transaction details (date, description, amount, balance after). It converts unstructured PDF statements into machine-readable JSON for reconciliation, reporting, and compliance workflows.

## When to use this

- **Account reconciliation**: Compare extracted transactions against internal records to detect discrepancies.
- **Financial reporting & compliance**: Automate audit trails, regulatory filings, and statement verification.
- **Transaction categorization**: Feed extracted descriptions and amounts into downstream ML classifiers for spending analysis.
- **Fraud detection**: Parse transaction history to identify suspicious patterns, duplicates, or anomalies.
- **Multi-bank consolidation**: Normalize statements from different banks into a single schema for portfolio views.

## Processor pipeline

### Step 1: Parse (agentic OCR + document chunking)
**Processor**: `parse` with `agentic_ocr` mode  
**Purpose**: Convert PDF to Markdown, preserving layout and capturing complex text (handwritten notes, signatures, table structures).  
**Config choice**: `blockOptions.text.agentic.enabled = true` + `chunkingStrategy.type = "document"` ensures the entire statement stays as one chunk (no page breaks), giving extraction context to infer transaction relationships and balances.  
**Why**: Bank statements are dense, tabular documents. Agentic OCR handles scanned/low-quality PDFs; document-level chunking prevents splitting mid-transaction table.

### Step 2: Extract (structured JSON with schema)
**Processor**: `extract` with `extraction_performance` baseProcessor  
**Purpose**: Pull account metadata, balances, and all transactions into a strict JSON object with fields: `bank_name`, `account_holder_name`, `account_number`, `account_type`, `opening_balance`, `closing_balance`, `statement_period_start_date`, `statement_period_end_date`, `total_credits`, `total_debits`, and `transactions[]` array.  
**Config choice**: Full schema defined below with ISO 4217 currency codes, nullable amounts for edge cases (refunds, reversals), and detailed transaction metadata (reference numbers, balance after each transaction).  
**Why**: Bank statements contain precise financial state; extraction schema enforces data type consistency and prevents garbage output. Array nesting captures all 40+ transactions in one call.

## TypeScript implementation



## CLI equivalent

```bash
# Parse the statement to markdown
extend parse statement.pdf \
  --block-options '{
    "text": {
      "agentic": {
        "enabled": true
      }
    }
  }' \
  --chunking-strategy document

# Extract structured data using the schema JSON
extend extract statement.pdf \
  --schema bank-statement-schema.json \
  --base-processor extraction_performance
```

**bank-statement-schema.json**:
```json
{
  "type": "object",
  "properties": {
    "bank_name": { "type": ["string", "null"], "description": "Full legal name of the bank" },
    "account_holder_name": { "type": ["string", "null"], "description": "Account holder name(s)" },
    "account_number": { "type": ["string", "null"], "description": "Account number (may be masked)" },
    "account_type": { "type": ["string", "null"], "description": "Checking, Savings, etc." },
    "branch_address": { "type": ["string", "null"], "description": "Branch or mailing address" },
    "statement_period_start_date": { "type": ["string", "null"], "description": "Start date (yyyy-mm-dd)" },
    "statement_period_end_date": { "type": ["string", "null"], "description": "End date (yyyy-mm-dd)" },
    "statement_issue_date": { "type": ["string", "null"], "description": "Issue date (yyyy-mm-dd)" },
    "opening_balance": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
    "closing_balance": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
    "total_credits": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
    "total_debits": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
    "transactions": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "transaction_date": { "type": ["string", "null"], "description": "Date (yyyy-mm-dd)" },
          "description": { "type": ["string", "null"], "description": "Transaction description" },
          "transaction_type": { "type": "string", "enum": ["debit", "credit", "fee", "interest", "other"] },
          "amount": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
          "balance_after_transaction": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
          "reference_number": { "type": ["string", "null"], "description": "Confirmation or transaction ID" }
        }
      }
    }
  }
}
```

## Schema

The extraction schema is a **Zod object** with these top-level fields:

| Field | Type | Nullable | Notes |
|-------|------|----------|-------|
| `bank_name` | string | ✓ | Full legal bank name from statement header |
| `account_holder_name` | string | ✓ | Primary and joint account owners if applicable |
| `account_number` | string | ✓ | Full or masked account number |
| `account_type` | string | ✓ | e.g., "Checking", "Savings", "Money Market" |
| `branch_address` | string | ✓ | Bank branch or statement mailing address |
| `statement_period_start_date` | ISO date | ✗ | First day of statement (yyyy-mm-dd) |
| `statement_period_end_date` | ISO date | ✗ | Last day of statement (yyyy-mm-dd) |
| `statement_issue_date` | ISO date | ✓ | Date statement was printed (if different from period end) |
| `opening_balance` | currency | ✗ | Balance at period start; includes `{ amount, iso_4217_currency_code }` |
| `closing_balance` | currency | ✗ | Balance at period end; same structure |
| `total_credits` | currency | ✗ | Sum of all deposits/credits |
| `total_debits` | currency | ✗ | Sum of all withdrawals/debits (usually negative) |
| `transactions` | array | ✗ | Array of transaction objects (see below) |

### Transaction object fields

| Field | Type | Nullable | Notes |
|-------|------|----------|-------|
| `transaction_date` | ISO date | ✗ | Date transaction posted (yyyy-mm-dd) |
| `description` | string | ✓ | As printed: merchant, check #, transfer detail, etc. |
| `transaction_type` | enum | ✗ | One of: `debit`, `credit`, `fee`, `interest`, `other` |
| `amount` | currency | ✗ | Signed value; negative = debit, positive = credit |
| `balance_after_transaction` | currency | ✗ | Running balance after this transaction |
| `reference_number` | string | ✓ | Confirmation #, check #, reference ID if printed |

---

## Accuracy tips

1. **Description field is highest-
import { ExtendClient, extendCurrency, extendDate } from "extend-ai";
import { z } from "zod";
import fs from "fs";

const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

// Define the Zod schema for bank statement extraction
const BankStatementSchema = z.object({
  // Account metadata
  bank_name: z.string().nullable().describe(
    "Full legal name of the bank that issued this statement, e.g., 'JPMorgan Chase Bank, N.A.'"
  ),
  account_holder_name: z.string().nullable().describe(
    "Full name(s) of the account holder(s) as printed on the statement, e.g., 'ANGIE M LOPEZ'"
  ),
  account_number: z.string().nullable().describe(
    "Account number, typically masked except last 4 digits, e.g., '000000861006133'"
  ),
  account_type: z.string().nullable().describe(
    "Type of account: Checking, Savings, Money Market, etc., e.g., 'High School Checking'"
  ),
  branch_address: z.string().nullable().describe(
    "Branch address or mailing address from the statement, e.g., 'P O Box 182051, Columbus, OH 43218-2051'"
  ),

  // Statement period and balances
  statement_period_start_date: extendDate().describe(
    "Start date of the statement period in ISO 8601 format (yyyy-mm-dd), e.g., '2020-12-22'"
  ),
  statement_period_end_date: extendDate().describe(
    "End date (closing date) of the statement period in ISO 8601 format (yyyy-mm-dd), e.g., '2021-01-25'"
  ),
  statement_issue_date: extendDate().nullable().describe(
    "Date the statement was issued/printed, if different from period end, in ISO 8601 format"
  ),

  opening_balance: extendCurrency().describe(
    "Beginning balance at the start of the statement period"
  ),
  closing_balance: extendCurrency().describe(
    "Ending balance at the close of the statement period"
  ),
  total_credits: extendCurrency().describe(
    "Sum of all deposits and credits during the period"
  ),
  total_debits: extendCurrency().describe(
    "Sum of all withdrawals and debits during the period (as a negative or absolute value)"
  ),

  // Transaction array
  transactions: z.array(
    z.object({
      transaction_date: extendDate().describe(
        "Date the transaction posted or was recorded, in ISO 8601 format (yyyy-mm-dd)"
      ),
      description: z.string().nullable().describe(
        "Transaction description as printed on the statement, e.g., 'Card Purchase 12/29 Apple.Com/US 800-676-2775 CA'"
      ),
      transaction_type: z.enum(["debit", "credit", "fee", "interest", "other"]).describe(
        "Type of transaction: debit (withdrawal/purchase), credit (deposit), fee (service/overdraft fee), interest (earned interest), or other"
      ),
      amount: extendCurrency().describe(
        "Transaction amount with sign: negative for debits, positive for credits"
      ),
      balance_after_transaction: extendCurrency().describe(
        "Running account balance immediately after this transaction posted"
      ),
      reference_number: z.string().nullable().describe(
        "Reference, confirmation, or transaction ID if printed on statement, e.g., 'Web ID: 2510020270'"
      ),
    })
  ).describe(
    "Array of all transactions in the statement period, in chronological order"
  ),
});

export async function processBankStatement(filePath: string) {
  try {
    console.log(`Processing bank statement: ${filePath}`);

    // Step 1: Read the file and convert to base64 data URL
    // (Extend SDK requires a URL; we use data: URL for local files)
    const fileBuffer = fs.readFileSync(filePath);
    const base64Data = fileBuffer.toString("base64");
    const dataUrl = `data:application/pdf;base64,${base64Data}`;

    // Step 2: Parse the document (agentic OCR + document-level chunking)
    console.log("Step 1: Parsing bank statement with agentic OCR...");
    const parseRun = await client.parseRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        parseConfig: {
          blockOptions: {
            text: {
              agentic: {
                enabled: true,
              },
            },
          },
          chunkingStrategy: {
            type: "document",
          },
        },
      },
    });

    if (parseRun.status !== "PROCESSED") {
      throw new Error(`Parse failed with status: ${parseRun.status}`);
    }

    console.log(
      `✓ Parsed successfully. Chunks: ${parseRun.output?.chunks?.length || 0}`
    );

    // Step 3: Extract structured data using the Zod schema
    console.log("Step 2: Extracting structured bank statement data...");
    const extractRun = await client.extractRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        schema: BankStatementSchema,
      },
    });

    if (extractRun.status !== "PROCESSED") {
      throw new Error(`Extract failed with status: ${extractRun.status}`);
    }

    const statement = extractRun.output?.value;

    console.log("\n✓ Extraction complete!\n");
    console.log("=== BANK STATEMENT SUMMARY ===");
    console.log(`Bank: ${statement?.bank_name}`);
    console.log(`Account Holder: ${statement?.account_holder_name}`);
    console.log(`Account Number: ${statement?.account_number}`);
    console.log(`Account Type: ${statement?.account_type}`);
    console.log(
      `Period: ${statement?.statement_period_start_date} to ${statement?.statement_period_end_date}`
    );
    console.log(
      `Opening Balance: ${statement?.opening_balance?.amount} ${statement?.opening_balance?.iso_4217_currency_code}`
    );
    console.log(
      `Closing Balance: ${statement?.closing_balance?.amount} ${statement?.closing_balance?.iso_4217_currency_code}`
    );
    console.log(
      `Total Credits: ${statement?.total_credits?.amount} ${statement?.total_credits?.iso_4217_currency_code}`
    );
    console.log(
      `Total Debits: ${statement?.total_debits?.amount} ${statement?.total_debits?.iso_4217_currency_code}`
    );
    console.log(`Transaction Count: ${statement?.transactions?.length || 0}`);

    // Display first 5 transactions as examples
    if (statement?.transactions && statement.transactions.length > 0) {
      console.log("\nFirst 5 Transactions:");
      statement.transactions.slice(0, 5).forEach((txn, idx) => {
        console.log(
          `  ${idx + 1}. [${txn.transaction_date}] ${txn.transaction_type.toUpperCase()} ${txn.amount?.amount} ${txn.description}`
        );
      });
    }

    // Return the full structured statement
    return statement;
  } catch (error) {
    console.error("Error processing bank statement:", error);
    throw error;
  }
}

// Allow direct invocation if this file is run as main
if (process.argv[2]) {
  processBankStatement(process.argv[2])
    .then(() => console.log("\n✓ Done!"))
    .catch((err) => {
      console.error(err.message || err);
      process.exit(1);
    });
}
import os
import base64
from pathlib import Path
from typing import Any
from extend_ai import Extend

client = Extend(token=os.environ["EXTEND_API_KEY"])

# Define the extraction schema matching the bank statement data model
bank_statement_schema = {
    "type": "object",
    "properties": {
        "bank_name": {
            "type": ["string", "null"],
            "description": "Name of the bank issuing the statement"
        },
        "statement_period_start": {
            "type": ["string", "null"],
            "description": "Start date of the statement period in MM/DD/YYYY format"
        },
        "statement_period_end": {
            "type": ["string", "null"],
            "description": "End date of the statement period in MM/DD/YYYY format"
        },
        "account_number": {
            "type": ["string", "null"],
            "description": "Customer account number"
        },
        "account_holder_names": {
            "type": ["string", "null"],
            "description": "Names of account holders"
        },
        "account_type": {
            "type": ["string", "null"],
            "description": "Type of account (e.g., High School Checking)"
        },
        "beginning_balance": {
            "type": ["string", "null"],
            "description": "Starting balance in currency format"
        },
        "ending_balance": {
            "type": ["string", "null"],
            "description": "Ending balance in currency format"
        },
        "total_deposits": {
            "type": ["string", "null"],
            "description": "Total deposits and additions"
        },
        "total_withdrawals": {
            "type": ["string", "null"],
            "description": "Combined total of all withdrawals"
        },
        "total_fees": {
            "type": ["string", "null"],
            "description": "Total fees charged during period"
        },
        "transactions": {
            "type": "array",
            "description": "List of transactions with date, description, and amount"
        }
    }
}


def process_bank_statement(file_path: str) -> Any:
    """Process a bank statement PDF and extract structured data."""
    try:
        # Read file and convert to base64 data URL
        file_buffer = Path(file_path).read_bytes()
        base64_str = base64.b64encode(file_buffer).decode("utf-8")
        mime_type = "application/pdf" if file_path.endswith(".pdf") else "application/octet-stream"
        data_url = f"data:{mime_type};base64,{base64_str}"

        print(f"Processing bank statement: {Path(file_path).name}")

        # Step 1: Parse the bank statement with agentic OCR
        print("Step 1: Parsing document...")
        parse_run = client.parse_runs.create_and_poll(
            file={"url": data_url},
            config={
                "block_options": {
                    "text": {
                        "agentic": {
                            "enabled": True,
                        },
                    },
                },
                "chunking_strategy": {
                    "type": "document",
                },
            },
        )

        if parse_run.status != "PROCESSED":
            raise Exception(f"Parse failed with status: {parse_run.status}")

        print(f"✓ Parsed {len(parse_run.output.chunks)} chunks")
        markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)
        print(f"✓ Markdown preview (first 500 chars):\n{markdown[:500]}...\n")

        # Step 2: Extract structured data from the parsed document
        print("Step 2: Extracting bank statement fields...")
        extract_run = client.extract_runs.create_and_poll(
            file={"url": data_url},
            config={
                "schema": bank_statement_schema,
            },
        )

        if extract_run.status != "PROCESSED":
            raise Exception(f"Extraction failed with status: {extract_run.status}")

        statement = extract_run.output.value

        # Output results
        print("\n✓ Extraction successful!\n")
        print("=== BANK STATEMENT SUMMARY ===")
        print(f"Bank: {statement.get('bank_name') or 'N/A'}")
        print(f"Account Holder: {statement.get('account_holder_names') or 'N/A'}")
        print(f"Account Number: {statement.get('account_number') or 'N/A'}")
        print(f"Account Type: {statement.get('account_type') or 'N/A'}")
        print(
            f"Statement Period: {statement.get('statement_period_start') or 'N/A'} to {statement.get('statement_period_end') or 'N/A'}"
        )
        print("\nBalance Summary:")
        print(f"  Opening: {statement.get('beginning_balance') or '0'}")
        print(f"  Closing: {statement.get('ending_balance') or '0'}")
        print(f"  Total Deposits: {statement.get('total_deposits') or '0'}")
        print(f"  Total Withdrawals: {statement.get('total_withdrawals') or '0'}")
        print(f"  Total Fees: {statement.get('total_fees') or '0'}")

        transactions = statement.get('transactions', [])
        print(f"\nTransactions: {len(transactions)} found")
        if transactions:
            print("\nFirst 5 transactions:")
            for idx, txn in enumerate(transactions[:5], 1):
                print(f"  {idx}. {txn}")

        # Return full structured data for downstream processing
        print("\n✓ Full statement data available for integration")
        return statement

    except Exception as error:
        print(f"Error processing bank statement: {error}")
        raise


if __name__ == "__main__":
    import sys
    if len(sys.argv) > 1:
        file_path = sys.argv[1]
        try:
            process_bank_statement(file_path)
        except Exception as err:
            print(f"Fatal error: {err}")
            sys.exit(1)
    else:
        print("Usage: python solution.py <path-to-bank-statement.pdf>")
// This code uses the Extend REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints with java.net.http.HttpClient (no external dependencies).

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;
import java.util.List;

public class BankStatementProcessor {

    private static final String API_BASE_URL = "https://api.extend.ai";
    private static final String API_KEY = System.getenv("EXTEND_API_KEY");
    private static final HttpClient httpClient = HttpClient.newHttpClient();

    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            System.out.println("Usage: java BankStatementProcessor <path-to-bank-statement.pdf>");
            System.exit(1);
        }
        
        String filePath = args[0];
        try {
            processBankStatement(filePath);
        } catch (Exception e) {
            System.err.println("Fatal error: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }

    public static void processBankStatement(String filePath) throws Exception {
        try {
            // Read file and convert to base64 data URL
            byte[] fileBuffer = Files.readAllBytes(Paths.get(filePath));
            String base64 = Base64.getEncoder().encodeToString(fileBuffer);
            String mimeType = filePath.endsWith(".pdf") ? "application/pdf" : "application/octet-stream";
            String dataUrl = "data:" + mimeType + ";base64," + base64;

            String fileName = Paths.get(filePath).getFileName().toString();
            System.out.println("Processing bank statement: " + fileName);

            // Step 1: Parse the bank statement with agentic OCR
            System.out.println("Step 1: Parsing document...");
            Map<String, Object> parseResponse = createAndPollParseRun(dataUrl);
            
            if (!"PROCESSED".equals(parseResponse.get("status"))) {
                throw new Exception("Parse failed with status: " + parseResponse.get("status"));
            }

            Map<String, Object> parseOutput = (Map<String, Object>) parseResponse.get("output");
            List<Map<String, Object>> chunks = (List<Map<String, Object>>) parseOutput.get("chunks");
            
            System.out.println("✓ Parsed " + chunks.size() + " chunks");
            StringBuilder markdown = new StringBuilder();
            for (Map<String, Object> chunk : chunks) {
                markdown.append(chunk.get("content")).append("\n\n");
            }
            String markdownStr = markdown.toString();
            System.out.println("✓ Markdown preview (first 500 chars):\n" + 
                markdownStr.substring(0, Math.min(500, markdownStr.length())) + "...\n");

            // Step 2: Extract structured data from the parsed document
            System.out.println("Step 2: Extracting bank statement fields...");
            Map<String, Object> extractResponse = createAndPollExtractRun(dataUrl);
            
            if (!"PROCESSED".equals(extractResponse.get("status"))) {
                throw new Exception("Extraction failed with status: " + extractResponse.get("status"));
            }

            Map<String, Object> extractOutput = (Map<String, Object>) extractResponse.get("output");
            Map<String, Object> statement = (Map<String, Object>) extractOutput.get("value");

            // Output results
            System.out.println("\n✓ Extraction successful!\n");
            System.out.println("=== BANK STATEMENT SUMMARY ===");
            System.out.println("Bank: " + (statement.get("bank_name") != null ? statement.get("bank_name") : "N/A"));
            System.out.println("Account Holder: " + (statement.get("account_holder_names") != null ? statement.get("account_holder_names") : "N/A"));
            System.out.println("Account Number: " + (statement.get("account_number") != null ? statement.get("account_number") : "N/A"));
            System.out.println("Account Type: " + (statement.get("account_type") != null ? statement.get("account_type") : "N/A"));
            System.out.println("Statement Period: " + 
                (statement.get("statement_period_start") != null ? statement.get("statement_period_start") : "N/A") + 
                " to " + 
                (statement.get("statement_period_end") != null ? statement.get("statement_period_end") : "N/A"));

            System.out.println("\nBalance Summary:");
            System.out.println("  Opening: " + (statement.get("beginning_balance") != null ? statement.get("beginning_balance") : "0") + " USD");
            System.out.println("  Closing: " + (statement.get("ending_balance") != null ? statement.get("ending_balance") : "0") + " USD");
            System.out.println("  Total Deposits: " + (statement.get("total_deposits") != null ? statement.get("total_deposits") : "0") + " USD");
            System.out.println("  Total Withdrawals: " + (statement.get("total_withdrawals") != null ? statement.get("total_withdrawals") : "0") + " USD");
            System.out.println("  Total Fees: " + (statement.get("total_fees") != null ? statement.get("total_fees") : "0") + " USD");

            List<Map<String, Object>> transactions = (List<Map<String, Object>>) statement.get("transactions");
            int txnCount = transactions != null ? transactions.size() : 0;
            System.out.println("\nTransactions: " + txnCount + " found");
            
            if (txnCount > 0) {
                System.out.println("\nFirst 5 transactions:");
                for (int i = 0; i < Math.min(5, txnCount); i++) {
                    Map<String, Object> txn = transactions.get(i);
                    System.out.println("  " + (i + 1) + ". [" + txn.get("date") + "] " + 
                        (txn.get("type") != null ? txn.get("type").toString().toUpperCase() : "UNKNOWN") + " " + 
                        (txn.get("description") != null ? txn.get("description") : ""));
                    System.out.println("     Amount: " + (txn.get("amount") != null ? txn.get("amount") : "0") + 
                        " USD");
                }
            }

            System.out.println("\n✓ Full statement data available for integration");

        } catch (Exception e) {
            System.err.println("Error processing bank statement: " + e.getMessage());
            throw e;
        }
    }

    private static Map<String, Object> createAndPollParseRun(String dataUrl) throws Exception {
        String requestBody = String.format(
            "{\"file\":{\"url\":\"%s\"},\"config\":{\"blockOptions\":{\"text\":{\"agentic\":{\"enabled\":true}}},\"chunkingStrategy\":{\"type\":\"document\"}}}",
            escapeJson(dataUrl)
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_BASE_URL + "/v1/parse-runs"))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() != 200 && response.statusCode() != 201) {
            throw new Exception("Parse API request failed with status " + response.statusCode() + ": " + response.body());
        }

        return pollForCompletion(parseJsonResponse(response.body()).get("id").toString(), "parse-runs");
    }

    private static Map<String, Object> createAndPollExtractRun(String dataUrl) throws Exception {
        String schemaJson = "{\"type\":\"object\",\"properties\":{\"bank_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the bank issuing the statement\"},\"statement_period_start\":{\"type\":[\"string\",\"null\"],\"description\":\"Start date of the statement period in MM/DD/YYYY format\"},\"statement_period_end\":{\"type\":[\"string\",\"null\"],\"description\":\"End date of the statement period in MM/DD/YYYY format\"},\"account_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Customer account number\"},\"account_holder_names\":{\"type\":[\"string\",\"null\"],\"description\":\"Names of account holders\"},\"account_type\":{\"type\":[\"string\",\"null\"],\"description\":\"Type of account (e.g., High School Checking)\"},\"beginning_balance\":{\"type\":[\"string\",\"null\"],\"description\":\"Starting balance in currency format\"},\"ending_balance\":{\"type\":[\"string\",\"null\"],\"description\":\"Ending balance in currency format\"},\"total_deposits\":{\"type\":[\"string\",\"null\"],\"description\":\"Total deposits and additions\"},\"total_withdrawals\":{\"type\":[\"string\",\"null\"],\"description\":\"Combined total of all withdrawals\"},\"total_fees\":{\"type\":[\"string\",\"null\"],\"description\":\"Total fees charged during period\"},\"transactions\":{\"type\":\"array\",\"description\":\"List of transactions with date, description, and amount\"}}}";
        
        String requestBody = String.format(
            "{\"file\":{\"url\":\"%s\"},\"config\":{\"schema\":%s}}",
            escapeJson(dataUrl),
            schemaJson
        );

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_BASE_URL + "/v1/extract-runs"))
            .header("Authorization", "Bearer " + API_KEY)
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(requestBody))
            .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() != 200 && response.statusCode() != 201) {
            throw new Exception("Extract API request failed with status " + response.statusCode() + ": " + response.body());
        }

        return pollForCompletion(parseJsonResponse(response.body()).get("id").toString(), "extract-runs");
    }

    private static Map<String, Object> pollForCompletion(String runId, String runType) throws Exception {
        int maxAttempts = 120;
        int attempt = 0;
        
        while (attempt < maxAttempts) {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE_URL + "/v1/" + runType + "/" + runId))
                .header("Authorization", "Bearer " + API_KEY)
                .GET()
                .build();

            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            
            if (response.statusCode() != 200) {
                throw new Exception("Poll request failed with status " + response.statusCode());
            }

            Map<String, Object> result = parseJsonResponse(response.body());
            String status = result.get("status").toString();
            
            if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
                return result;
            }

            Thread.sleep(1000);
            attempt++;
        }

        throw new Exception("Polling timeout for " + runType);
    }

    private static Map<String, Object> parseJsonResponse(String json) {
        Map<String, Object> result = new java.util.HashMap<>();
        
        if (json.contains("\"id\"")) {
            String id = extractJsonString(json, "id");
            result.put("id", id);
        }
        if (json.contains("\"status\"")) {
            String status = extractJsonString(json, "status");
            result.put("status", status);
        }
        if (json.contains("\"output\"")) {
            result.put("output", parseJsonObject(json, "output"));
        }
        
        return result;
    }

    private static Map<String, Object> parseJsonObject(String json, String key) {
        Map<String, Object> obj = new java.util.HashMap<>();
        int startIdx = json.indexOf("\"" + key + "\"");
        if (startIdx == -1) return obj;
        
        int colonIdx = json.indexOf(":", startIdx);
        int braceIdx = json.indexOf("{", colonIdx);
        if (braceIdx == -1) return obj;
        
        int depth = 0;
        int endIdx = braceIdx;
        for (int i = braceIdx; i < json.length(); i++) {
            if (json.charAt(i) == '{') depth++;
            if (json.charAt(i) == '}') {
                depth--;
                if (depth == 0) {
                    endIdx = i;
                    break;
                }
            }
        }
        
        String objStr = json.substring(braceIdx, endIdx + 1);
        
        if (objStr.contains("\"chunks\"")) {
            obj.put("chunks", parseJsonArray(objStr, "chunks"));
        }
        if (objStr.contains("\"value\"")) {
            obj.put("value", parseJsonValue(objStr, "value"));
        }
        
        return obj;
    }

    private static List<Map<String, Object>> parseJsonArray(String json, String key) {
        List<Map<String, Object>> list = new java.util.ArrayList<>();
        int startIdx = json.indexOf("\"" + key + "\"");
        if (startIdx == -1) return list;
        
        int bracketIdx = json.indexOf("[", startIdx);
        if (bracketIdx == -1) return list;
        
        int depth = 0;
        int endIdx = bracketIdx;
        for (int i = bracketIdx; i < json.length(); i++) {
            if (json.charAt(i) == '[') depth++;
            if (json.charAt(i) == ']') {
                depth--;
                if (depth == 0) {
                    endIdx = i;
                    break;
                }
            }
        }
        
        String arrayStr = json.substring(bracketIdx + 1, endIdx);
        String[] items = arrayStr.split("(?<=}),(?={)");
        
        for (String item : items) {
            if (item.trim().isEmpty()) continue;
            Map<String, Object> obj = new java.util.HashMap<>();
            if (item.contains("\"content\"")) {
                obj.put("content", extractJsonString(item, "content"));
            }
            list.add(obj);
        }
        
        return list;
    }

    private static Map<String, Object> parseJsonValue(String json, String key) {
        Map<String, Object> obj = new java.util.HashMap<>();
        int startIdx = json.indexOf("\"" + key + "\"");
        if (startIdx == -1) return obj;
        
        int colonIdx = json.indexOf(":", startIdx);
        int braceIdx = json.indexOf("{", colonIdx);
        if (braceIdx == -1) return obj;
        
        int depth = 0;
        int endIdx = braceIdx;
        for (int i = braceIdx; i < json.length(); i++) {
            if (json.charAt(i) == '{') depth++;
            if (json.charAt(i) == '}') {
                depth--;
                if (depth == 0) {
                    endIdx = i;
                    break;
                }
            }
        }
        
        String objStr = json.substring(braceIdx, endIdx + 1);
        
        String[] fields = {"bank_name", "account_holder_names", "account_number", "account_type",
                          "statement_period_start", "statement_period_end", "beginning_balance",
                          "ending_balance", "total_deposits", "total_withdrawals", "total_fees"};
        
        for (String field : fields) {
            if (objStr.contains("\"" + field + "\"")) {
                String value = extractJsonString(objStr, field);
                obj.put(field, value);
            }
        }
        
        if (objStr.contains("\"transactions\"")) {
            obj.put("transactions", parseJsonArray(objStr, "transactions"));
        }
        
        return obj;
    }

    private static String extractJsonString(String json, String key) {
        int startIdx = json.indexOf("\"" + key + "\"");
        if (startIdx == -1) return null;
        
        int colonIdx = json.indexOf(":", startIdx);
        int quoteIdx = json.indexOf("\"", colonIdx);
        if (quoteIdx == -1) return null;
        
        int endIdx = quoteIdx + 1;
        while (endIdx < json.length()) {
            if (json.charAt(endIdx) == '"' && json.charAt(endIdx - 1) != '\\') {
                break;
            }
            endIdx++;
        }
        
        return json.substring(quoteIdx + 1, endIdx);
    }

    private static String escapeJson(String str) {
        return str.replace("\\", "\\\\")
                  .replace("\"", "\\\"")
                  .replace("\n", "\\n")
                  .replace("\r", "\\r")
                  .replace("\t", "\\t");
    }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.

package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const extendAPIBase = "https://api.extend.ai"

type Currency struct {
	Amount              float64 `json:"amount"`
	ISO4217CurrencyCode string  `json:"iso_4217_currency_code"`
}

type Transaction struct {
	TransactionDate         string    `json:"transaction_date"`
	TransactionType         *string   `json:"transaction_type"`
	Description             *string   `json:"description"`
	Amount                  *Currency `json:"amount"`
	ReferenceNumber         *string   `json:"reference_number"`
	BalanceAfterTransaction *Currency `json:"balance_after_transaction"`
}

type BankStatement struct {
	BankName                  *string        `json:"bank_name"`
	AccountHolderName         *string        `json:"account_holder_name"`
	AccountNumber             *string        `json:"account_number"`
	AccountType               *string        `json:"account_type"`
	StatementPeriodStartDate  *string        `json:"statement_period_start_date"`
	StatementPeriodEndDate    *string        `json:"statement_period_end_date"`
	OpeningBalance            *Currency      `json:"opening_balance"`
	ClosingBalance            *Currency      `json:"closing_balance"`
	TotalDebits               *Currency      `json:"total_debits"`
	TotalCredits              *Currency      `json:"total_credits"`
	Transactions              []Transaction  `json:"transactions"`
	BranchAddress             *string        `json:"branch_address"`
}

type ParseRunRequest struct {
	File   FileInput      `json:"file"`
	Config ParseConfig    `json:"config"`
}

type FileInput struct {
	URL string `json:"url"`
}

type ParseConfig struct {
	BlockOptions     BlockOptions     `json:"blockOptions"`
	ChunkingStrategy ChunkingStrategy `json:"chunkingStrategy"`
}

type BlockOptions struct {
	Text TextBlock `json:"text"`
}

type TextBlock struct {
	Agentic AgenticConfig `json:"agentic"`
}

type AgenticConfig struct {
	Enabled bool `json:"enabled"`
}

type ChunkingStrategy struct {
	Type string `json:"type"`
}

type ParseRunResponse struct {
	ID     string      `json:"id"`
	Status string      `json:"status"`
	Output ParseOutput `json:"output"`
}

type ParseOutput struct {
	Chunks []Chunk `json:"chunks"`
}

type Chunk struct {
	Content string `json:"content"`
}

type ExtractRunRequest struct {
	File   FileInput     `json:"file"`
	Config ExtractConfig `json:"config"`
}

type ExtractConfig struct {
	Schema map[string]interface{} `json:"schema"`
}

type ExtractRunResponse struct {
	ID     string        `json:"id"`
	Status string        `json:"status"`
	Output ExtractOutput `json:"output"`
}

type ExtractOutput struct {
	Value BankStatement `json:"value"`
}

type RunStatusResponse struct {
	ID     string          `json:"id"`
	Status string          `json:"status"`
	Output json.RawMessage `json:"output"`
}

func makeRequest(method, endpoint string, body interface{}, token string) ([]byte, error) {
	url := extendAPIBase + endpoint
	var reqBody io.Reader
	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewBuffer(jsonBody)
	}

	req, err := http.NewRequest(method, url, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(respBody))
	}

	return respBody, nil
}

func pollParseRun(runID, token string) (*ParseRunResponse, error) {
	for {
		respBody, err := makeRequest("GET", "/parse-runs/"+runID, nil, token)
		if err != nil {
			return nil, err
		}

		var run ParseRunResponse
		if err := json.Unmarshal(respBody, &run); err != nil {
			return nil, err
		}

		if run.Status == "PROCESSED" || run.Status == "FAILED" {
			return &run, nil
		}

		time.Sleep(2 * time.Second)
	}
}

func pollExtractRun(runID, token string) (*ExtractRunResponse, error) {
	for {
		respBody, err := makeRequest("GET", "/extract-runs/"+runID, nil, token)
		if err != nil {
			return nil, err
		}

		var run ExtractRunResponse
		if err := json.Unmarshal(respBody, &run); err != nil {
			return nil, err
		}

		if run.Status == "PROCESSED" || run.Status == "FAILED" {
			return &run, nil
		}

		time.Sleep(2 * time.Second)
	}
}

func processBankStatement(filePath string) (*BankStatement, error) {
	token := os.Getenv("EXTEND_API_KEY")
	if token == "" {
		return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
	}

	// Read file and convert to base64 data URL
	fileBuffer, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

	base64Str := base64.StdEncoding.EncodeToString(fileBuffer)
	mimeType := "application/octet-stream"
	if strings.HasSuffix(filePath, ".pdf") {
		mimeType = "application/pdf"
	}
	dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)

	fmt.Printf("Processing bank statement: %s\n", filepath.Base(filePath))

	// Step 1: Parse the bank statement with agentic OCR
	fmt.Println("Step 1: Parsing document...")
	parseReq := ParseRunRequest{
		File: FileInput{URL: dataURL},
		Config: ParseConfig{
			BlockOptions: BlockOptions{
				Text: TextBlock{
					Agentic: AgenticConfig{Enabled: true},
				},
			},
			ChunkingStrategy: ChunkingStrategy{Type: "document"},
		},
	}

	respBody, err := makeRequest("POST", "/parse-runs", parseReq, token)
	if err != nil {
		return nil, err
	}

	var parseInitResp ParseRunResponse
	if err := json.Unmarshal(respBody, &parseInitResp); err != nil {
		return nil, err
	}

	parseRun, err := pollParseRun(parseInitResp.ID, token)
	if err != nil {
		return nil, err
	}

	if parseRun.Status != "PROCESSED" {
		return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
	}

	fmt.Printf("✓ Parsed %d chunks\n", len(parseRun.Output.Chunks))
	var markdown strings.Builder
	for i, chunk := range parseRun.Output.Chunks {
		if i > 0 {
			markdown.WriteString("\n\n")
		}
		markdown.WriteString(chunk.Content)
	}
	markdownStr := markdown.String()
	if len(markdownStr) > 500 {
		fmt.Printf("✓ Markdown preview (first 500 chars):\n%s...\n\n", markdownStr[:500])
	} else {
		fmt.Printf("✓ Markdown preview:\n%s\n\n", markdownStr)
	}

	// Step 2: Extract structured data from the parsed document
	fmt.Println("Step 2: Extracting bank statement fields...")
	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"bank_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Full legal name of the bank (e.g., 'JPMorgan Chase Bank, N.A.')",
			},
			"account_holder_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Name(s) of the primary account holder(s)",
			},
			"account_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Account number, typically last 4 digits visible for security",
			},
			"account_type": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Type of account (e.g., 'Checking', 'Savings', 'High School Checking')",
			},
			"statement_period_start_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Start date of the statement period in ISO 8601 format (YYYY-MM-DD)",
			},
			"statement_period_end_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "End date (closing date) of the statement period in ISO 8601 format (YYYY-MM-DD)",
			},
			"opening_balance": map[string]interface{}{
				"type":        "object",
				"description": "Account balance at the beginning of the statement period",
			},
			"closing_balance": map[string]interface{}{
				"type":        "object",
				"description": "Account balance at the end of the statement period",
			},
			"total_debits": map[string]interface{}{
				"type":        "object",
				"description": "Sum of all debit transactions (withdrawals, purchases, fees)",
			},
			"total_credits": map[string]interface{}{
				"type":        "object",
				"description": "Sum of all credit transactions (deposits, refunds, transfers in)",
			},
			"transactions": map[string]interface{}{
				"type":        "array",
				"description": "All transactions in chronological order for the statement period",
			},
			"branch_address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Bank branch address or mailing address from statement header",
			},
		},
	}

	extractReq := ExtractRunRequest{
		File: FileInput{URL: dataURL},
		Config: ExtractConfig{Schema: schema},
	}

	respBody, err = makeRequest("POST", "/extract-runs", extractReq, token)
	if err != nil {
		return nil, err
	}

	var extractInitResp ExtractRunResponse
	if err := json.Unmarshal(respBody, &extractInitResp); err != nil {
		return nil, err
	}

	extractRun, err := pollExtractRun(extractInitResp.ID, token)
	if err != nil {
		return nil, err
	}

	if extractRun.Status != "PROCESSED" {
		return nil, fmt.Errorf("extraction failed with status: %s", extractRun.Status)
	}

	statement := &extractRun.Output.Value

	// Output results
	fmt.Println("\n✓ Extraction successful!\n")
	fmt.Println("=== BANK STATEMENT SUMMARY ===")
	if statement.BankName != nil {
		fmt.Printf("Bank: %s\n", *statement.BankName)
	} else {
		fmt.Println("Bank: N/A")
	}
	if statement.AccountHolderName != nil {
		fmt.Printf("Account Holder: %s\n", *statement.AccountHolderName)
	} else {
		fmt.Println("Account Holder: N/A")
	}
	if statement.AccountNumber != nil {
		fmt.Printf("Account Number: %s\n", *statement.AccountNumber)
	} else {
		fmt.Println("Account Number: N/A")
	}
	if statement.AccountType != nil {
		fmt.Printf("Account Type: %s\n", *statement.AccountType)
	} else {
		fmt.Println("Account Type: N/A")
	}

	startDate := "N/A"
	if statement.StatementPeriodStartDate != nil {
		startDate = *statement.StatementPeriodStartDate
	}
	endDate := "N/A"
	if statement.StatementPeriodEndDate != nil {
		endDate = *statement.StatementPeriodEndDate
	}
	fmt.Printf("Statement Period: %s to %s\n", startDate, endDate)

	fmt.Println("\nBalance Summary:")
	if statement.OpeningBalance != nil {
		fmt.Printf("  Opening: %v %s\n", statement.OpeningBalance.Amount, statement.OpeningBalance.ISO4217CurrencyCode)
	} else {
		fmt.Println("  Opening: 0 USD")
	}
	if statement.ClosingBalance != nil {
		fmt.Printf("  Closing: %v %s\n", statement.ClosingBalance.Amount, statement.ClosingBalance.ISO4217CurrencyCode)
	} else {
		fmt.Println("  Closing: 0 USD")
	}
	if statement.TotalDebits != nil {
		fmt.Printf("  Total Debits: %v %s\n", statement.TotalDebits.Amount, statement.TotalDebits.ISO4217CurrencyCode)
	} else {
		fmt.Println("  Total Debits: 0 USD")
	}
	if statement.TotalCredits != nil {
		fmt.Printf("  Total Credits: %v %s\n", statement.TotalCredits.Amount, statement.TotalCredits.ISO4217CurrencyCode)
	} else {
		fmt.Println("  Total Credits: 0 USD")
	}

	fmt.Printf("\nTransactions: %d found\n", len(statement.Transactions))
	if len(statement.Transactions) > 0 {
		fmt.Println("\nFirst 5 transactions:")
		limit := 5
		if len(statement.Transactions) < 5 {
			limit = len(statement.Transactions)
		}
		for i := 0; i < limit; i++ {
			txn := statement.Transactions[i]
			txnType := "UNKNOWN"
			if txn.TransactionType != nil {
				txnType = strings.ToUpper(*txn.TransactionType)
			}
			desc := "N/A"
			if txn.Description != nil {
				desc = *txn.Description
			}
			fmt.Printf("  %d. [%s] %s %s\n", i+1, txn.TransactionDate, txnType, desc)

			amount := 0.0
			currency := "USD"
			if txn.Amount != nil {
				amount = txn.Amount.Amount
				currency = txn.Amount.ISO4217CurrencyCode
			}
			balance := 0.0
			if txn.BalanceAfterTransaction != nil {
				balance = txn.BalanceAfterTransaction.Amount
			}
			fmt.Printf("     Amount: %v %s, Balance: %v\n", amount, currency, balance)
		}
	}

	fmt.Println("\n✓ Full statement data available for integration")
	return statement, nil
}

func main() {
	flag.Parse()
	args := flag.Args()

	if len(args) == 0 {
		fmt.Println("Usage: go run solution.go <path-to-bank-statement.pdf>")
		os.Exit(1)
	}

	filePath := args[0]
	_, err := processBankStatement(filePath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Fatal error: %v\n", err)
		os.Exit(1)
	}
}
// Deploy the "Bank Statement" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/bank-statement-extractor.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   npx tsx provision.ts
//
// Generated by doc1 (template: bank-statement-extractor).

import fs from "node:fs";
import path from "node:path";

const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }

const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "bank-statement-extractor.json");

type State = { workflowId?: string };
const state: State = fs.existsSync(STATE_FILE)
  ? JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
  : {};
function saveState() {
  fs.mkdirSync(STATE_DIR, { recursive: true });
  fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}

async function api(method: string, pathName: string, body?: unknown) {
  const res = await fetch(API + pathName, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "x-extend-api-version": VERSION,
      ...(body ? { "Content-Type": "application/json" } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${method} ${pathName} failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
  return data;
}

// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
  "name": "Bank Statement Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "bank_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the bank issuing the statement"
              },
              "statement_period_start": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Start date of the statement period in MM/DD/YYYY format"
              },
              "statement_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "End date of the statement period in MM/DD/YYYY format"
              },
              "account_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Customer account number"
              },
              "account_holder_names": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Names of account holders"
              },
              "account_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Type of account (e.g., High School Checking)"
              },
              "beginning_balance": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Starting balance in currency format"
              },
              "ending_balance": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Ending balance in currency format"
              },
              "total_deposits": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Total deposits and additions"
              },
              "total_withdrawals": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Combined total of all withdrawals"
              },
              "total_fees": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Total fees charged during period"
              },
              "transactions": {
                "type": "array",
                "description": "List of transactions with date, description, and amount"
              }
            }
          }
        }
      }
    }
  ]
};

async function main() {
  console.log(`Deploying "${WORKFLOW.name}"…`);

  if (state.workflowId) {
    console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
    await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
  } else {
    // Reuse an existing workflow with the same name if one exists (e.g. a
    // previous run's state file was lost) instead of creating a duplicate.
    try {
      const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
      const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
      const existing = items.find((x) => x.name === WORKFLOW.name);
      if (existing?.id) {
        state.workflowId = existing.id; saveState();
        console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
        await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
      }
    } catch { /* lookup is best-effort; fall through to create */ }

    if (!state.workflowId) {
      const created = await api("POST", "/workflows", WORKFLOW);
      const wfId = created.id ?? created.workflow?.id;
      if (!wfId) throw new Error("Could not read created workflow id from response");
      state.workflowId = wfId; saveState();
      console.log(`+ created workflow (${wfId})`);
    }
  }

  // Deploy the current draft as a new version so the workflow is runnable —
  // best-effort: some accounts/plans may not require this explicit step.
  await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});

  console.log("\nDone. Run documents through it with:");
  console.log(`  POST ${API}/workflow_runs  { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
  console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}

main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from typing import Any, Optional

from extend_ai import Extend

API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
    print("Set EXTEND_API_KEY first.", file=sys.stderr)
    sys.exit(1)

STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "bank-statement-extractor.json"

state: dict[str, Optional[str]] = {}
if STATE_FILE.exists():
    state = json.loads(STATE_FILE.read_text())


def save_state() -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, indent=2))


WORKFLOW = {
    "name": "Bank Statement Processing Pipeline",
    "steps": [
        {
            "name": "startTrigger1",
            "type": "TRIGGER",
            "next": [{"step": "parse1"}],
        },
        {
            "name": "parse1",
            "type": "PARSE",
            "config": {
                "parseConfig": {
                    "blockOptions": {
                        "text": {
                            "agentic": {
                                "enabled": True
                            }
                        }
                    },
                    "chunkingStrategy": {
                        "type": "document"
                    }
                }
            },
            "next": [{"step": "extraction2"}],
        },
        {
            "name": "extraction2",
            "type": "EXTRACT",
            "config": {
                "extractorConfig": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "bank_name": {
                                "type": ["string", "null"],
                                "description": "Name of the bank issuing the statement"
                            },
                            "statement_period_start": {
                                "type": ["string", "null"],
                                "description": "Start date of the statement period in MM/DD/YYYY format"
                            },
                            "statement_period_end": {
                                "type": ["string", "null"],
                                "description": "End date of the statement period in MM/DD/YYYY format"
                            },
                            "account_number": {
                                "type": ["string", "null"],
                                "description": "Customer account number"
                            },
                            "account_holder_names": {
                                "type": ["string", "null"],
                                "description": "Names of account holders"
                            },
                            "account_type": {
                                "type": ["string", "null"],
                                "description": "Type of account (e.g., High School Checking)"
                            },
                            "beginning_balance": {
                                "type": ["string", "null"],
                                "description": "Starting balance in currency format"
                            },
                            "ending_balance": {
                                "type": ["string", "null"],
                                "description": "Ending balance in currency format"
                            },
                            "total_deposits": {
                                "type": ["string", "null"],
                                "description": "Total deposits and additions"
                            },
                            "total_withdrawals": {
                                "type": ["string", "null"],
                                "description": "Combined total of all withdrawals"
                            },
                            "total_fees": {
                                "type": ["string", "null"],
                                "description": "Total fees charged during period"
                            },
                            "transactions": {
                                "type": "array",
                                "description": "List of transactions with date, description, and amount"
                            }
                        }
                    }
                }
            }
        }
    ]
}


def main() -> None:
    client = Extend(token=API_KEY)
    
    print(f"Deploying \"{WORKFLOW['name']}\"…")

    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
    else:
        # Try to find an existing workflow with the same name
        try:
            workflows = client.workflows.list(name=WORKFLOW["name"])
            items = workflows.data if hasattr(workflows, "data") else (workflows.items if hasattr(workflows, "items") else [])
            existing = next((w for w in items if w.name == WORKFLOW["name"]), None)
            if existing and existing.id:
                state["workflowId"] = existing.id
                save_state()
                print(f"✓ workflow \"{WORKFLOW['name']}\" found in your account ({existing.id}) — updating steps")
                client.workflows.update(id=existing.id, steps=WORKFLOW["steps"])
        except Exception:
            pass

        if not state.get("workflowId"):
            created = client.workflows.create(**WORKFLOW)
            workflow_id = created.id
            if not workflow_id:
                raise ValueError("Could not read created workflow id from response")
            state["workflowId"] = workflow_id
            save_state()
            print(f"+ created workflow ({workflow_id})")

    # Deploy the current draft as a new version
    try:
        client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass

    print("\nDone. Run documents through it with:")
    print(f"  POST https://api.extend.ai/workflow_runs  {{ \"workflow\": {{ \"id\": \"{state['workflowId']}\" }}, \"file\": {{ \"url\": \"https://…\" }} }}")
    print("Or open the workflow in the Extend dashboard to review and deploy it.")


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// This code calls Extend's REST API directly using only Java's built-in java.net.http.HttpClient.
// Extend does not publish an official Java SDK; this approach has zero external dependencies.

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class BankStatementProvisioner {
  private static final String API = "https://api.extend.ai";
  private static final String VERSION = "2026-02-09";
  private static final String API_KEY = System.getenv("EXTEND_API_KEY");
  private static final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
  private static final Path STATE_FILE = STATE_DIR.resolve("bank-statement-extractor.json");
  private static final HttpClient HTTP = HttpClient.newHttpClient();

  static class State {
    String workflowId;
  }

  private static State state = new State();

  public static void main(String[] args) throws Exception {
    if (API_KEY == null || API_KEY.isEmpty()) {
      System.err.println("Set EXTEND_API_KEY first.");
      System.exit(1);
    }

    loadState();

    Map<String, Object> workflow = buildWorkflow();
    System.out.println("Deploying \"" + workflow.get("name") + "\"…");

    if (state.workflowId != null && !state.workflowId.isEmpty()) {
      System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
      Map<String, Object> updateBody = new LinkedHashMap<>();
      updateBody.put("steps", workflow.get("steps"));
      api("POST", "/workflows/" + state.workflowId, updateBody);
    } else {
      try {
        String name = (String) workflow.get("name");
        String encoded = URLEncoder.encode(name, StandardCharsets.UTF_8);
        Map<String, Object> list = api("GET", "/workflows?name=" + encoded, null);
        List<?> items = (List<?>) (list.get("data") != null ? list.get("data") : list.get("items"));
        if (items == null) items = List.of();

        for (Object item : items) {
          if (item instanceof Map) {
            Map<?, ?> itemMap = (Map<?, ?>) item;
            if (name.equals(itemMap.get("name"))) {
              String id = (String) itemMap.get("id");
              if (id != null) {
                state.workflowId = id;
                saveState();
                System.out.println("✓ workflow \"" + name + "\" found in your account (" + id + ") — updating steps");
                Map<String, Object> updateBody = new LinkedHashMap<>();
                updateBody.put("steps", workflow.get("steps"));
                api("POST", "/workflows/" + id, updateBody);
                break;
              }
            }
          }
        }
      } catch (Exception e) {
        // lookup is best-effort; fall through to create
      }

      if (state.workflowId == null || state.workflowId.isEmpty()) {
        Map<String, Object> created = api("POST", "/workflows", workflow);
        String wfId = (String) created.get("id");
        if (wfId == null) {
          Map<?, ?> workflowObj = (Map<?, ?>) created.get("workflow");
          if (workflowObj != null) {
            wfId = (String) workflowObj.get("id");
          }
        }
        if (wfId == null) {
          throw new RuntimeException("Could not read created workflow id from response");
        }
        state.workflowId = wfId;
        saveState();
        System.out.println("+ created workflow (" + wfId + ")");
      }
    }

    try {
      api("POST", "/workflows/" + state.workflowId + "/versions", new LinkedHashMap<>());
    } catch (Exception e) {
      // best-effort: some accounts/plans may not require this explicit step
    }

    System.out.println("\nDone. Run documents through it with:");
    System.out.println("  POST " + API + "/workflow_runs  { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
    System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
  }

  private static void loadState() throws IOException {
    if (Files.exists(STATE_FILE)) {
      String json = Files.readString(STATE_FILE);
      state = parseStateJson(json);
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    String json = toJson(Map.of("workflowId", state.workflowId != null ? state.workflowId : ""));
    Files.writeString(STATE_FILE, json);
  }

  private static Map<String, Object> api(String method, String pathName, Map<String, Object> body) throws Exception {
    String url = API + pathName;
    HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url))
        .method(method, body != null ? HttpRequest.BodyPublishers.ofString(toJson(body)) : HttpRequest.BodyPublishers.noBody())
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

    if (body != null) {
      builder.header("Content-Type", "application/json");
    }

    HttpRequest request = builder.build();
    HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());

    Map<String, Object> data = parseJson(response.body());
    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      String errorMsg = toJson(data);
      if (errorMsg.length() > 300) {
        errorMsg = errorMsg.substring(0, 300);
      }
      throw new RuntimeException(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
    }

    return data;
  }

  private static Map<String, Object> buildWorkflow() {
    Map<String, Object> workflow = new LinkedHashMap<>();
    workflow.put("name", "Bank Statement Processing Pipeline");

    List<Map<String, Object>> steps = List.of(
        buildTriggerStep(),
        buildParseStep(),
        buildExtractionStep()
    );
    workflow.put("steps", steps);

    return workflow;
  }

  private static Map<String, Object> buildTriggerStep() {
    Map<String, Object> step = new LinkedHashMap<>();
    step.put("name", "startTrigger1");
    step.put("type", "TRIGGER");
    step.put("next", List.of(Map.of("step", "parse1")));
    return step;
  }

  private static Map<String, Object> buildParseStep() {
    Map<String, Object> step = new LinkedHashMap<>();
    step.put("name", "parse1");
    step.put("type", "PARSE");

    Map<String, Object> config = new LinkedHashMap<>();
    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();
    Map<String, Object> text = new LinkedHashMap<>();
    Map<String, Object> agentic = new LinkedHashMap<>();
    agentic.put("enabled", true);
    text.put("agentic", agentic);
    blockOptions.put("text", text);
    parseConfig.put("blockOptions", blockOptions);
    parseConfig.put("chunkingStrategy", Map.of("type", "document"));
    config.put("parseConfig", parseConfig);
    step.put("config", config);

    step.put("next", List.of(Map.of("step", "extraction2")));
    return step;
  }

  private static Map<String, Object> buildExtractionStep() {
    Map<String, Object> step = new LinkedHashMap<>();
    step.put("name", "extraction2");
    step.put("type", "EXTRACT");

    Map<String, Object> config = new LinkedHashMap<>();
    Map<String, Object> extractorConfig = new LinkedHashMap<>();
    Map<String, Object> schema = new LinkedHashMap<>();
    schema.put("type", "object");

    Map<String, Object> properties = new LinkedHashMap<>();
    properties.put("bank_name", Map.of("type", List.of("string", "null"), "description", "Name of the bank issuing the statement"));
    properties.put("statement_period_start", Map.of("type", List.of("string", "null"), "description", "Start date of the statement period in MM/DD/YYYY format"));
    properties.put("statement_period_end", Map.of("type", List.of("string", "null"), "description", "End date of the statement period in MM/DD/YYYY format"));
    properties.put("account_number", Map.of("type", List.of("string", "null"), "description", "Customer account number"));
    properties.put("account_holder_names", Map.of("type", List.of("string", "null"), "description", "Names of account holders"));
    properties.put("account_type", Map.of("type", List.of("string", "null"), "description", "Type of account (e.g., High School Checking)"));
    properties.put("beginning_balance", Map.of("type", List.of("string", "null"), "description", "Starting balance in currency format"));
    properties.put("ending_balance", Map.of("type", List.of("string", "null"), "description", "Ending balance in currency format"));
    properties.put("total_deposits", Map.of("type", List.of("string", "null"), "description", "Total deposits and additions"));
    properties.put("total_withdrawals", Map.of("type", List.of("string", "null"), "description", "Combined total of all withdrawals"));
    properties.put("total_fees", Map.of("type", List.of("string", "null"), "description", "Total fees charged during period"));
    properties.put("transactions", Map.of("type", "array", "description", "List of transactions with date, description, and amount"));

    schema.put("properties", properties);
    extractorConfig.put("schema", schema);
    config.put("extractorConfig", extractorConfig);
    step.put("config", config);

    return step;
  }

  private static String toJson(Object obj) {
    if (obj == null) return "null";
    if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
    if (obj instanceof Number) return obj.toString();
    if (obj instanceof Boolean) return obj.toString();
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      StringBuilder sb = new StringBuilder("{");
      boolean first = true;
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        if (!first) sb.append(",");
        sb.append("\"").append(escapeJson(entry.getKey().toString())).append("\":");
        sb.append(toJson(entry.getValue()));
        first = false;
      }
      sb.append("}");
      return sb.toString();
    }
    if (obj instanceof List) {
      List<?> list = (List<?>) obj;
      StringBuilder sb = new StringBuilder("[");
      boolean first = true;
      for (Object item : list) {
        if (!first) sb.append(",");
        sb.append(toJson(item));
        first = false;
      }
      sb.append("]");
      return sb.toString();
    }
    return "null";
  }

  private static String escapeJson(String s) {
    return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
  }

  private static Map<String, Object> parseJson(String json) {
    json = json.trim();
    if (json.isEmpty() || json.equals("{}")) {
      return new LinkedHashMap<>();
    }
    return parseJsonObject(json, new int[]{0});
  }

  private static Map<String, Object> parseJsonObject(String json, int[] pos) {
    Map<String, Object> map = new LinkedHashMap<>();
    skipWhitespace(json, pos);
    if (pos[0] >= json.length() || json.charAt(pos[0]) != '{') {
      return map;
    }
    pos[0]++;
    skipWhitespace(json, pos);

    while (pos[0] < json.length() && json.charAt(pos[0]) != '}') {
      skipWhitespace(json, pos);
      String key = parseJsonString(json, pos);
      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ':') {
        pos[0]++;
      }
      skipWhitespace(json, pos);
      Object value = parseJsonValue(json, pos);
      map.put(key, value);
      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
        pos[0]++;
      }
      skipWhitespace(json, pos);
    }
    if (pos[0] < json.length() && json.charAt(pos[0]) == '}') {
      pos[0]++;
    }
    return map;
  }

  private static List<?> parseJsonArray(String json, int[] pos) {
    List<Object> list = new java.util.ArrayList<>();
    skipWhitespace(json, pos);
    if (pos[0] >= json.length() || json.charAt(pos[0]) != '[') {
      return list;
    }
    pos[0]++;
    skipWhitespace(json, pos);

    while (pos[0] < json.length() && json.charAt(pos[0]) != ']') {
      Object value = parseJsonValue(json, pos);
      list.add(value);
      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
        pos[0]++;
      }
      skipWhitespace(json, pos);
    }
    if (pos[0] < json.length() && json.charAt(pos[0]) == ']') {
      pos[0]++;
    }
    return list;
  }

  private static Object parseJsonValue(String json, int[] pos) {
    skipWhitespace(json, pos);
    if (pos[0] >= json.length()) return null;

    char c = json.charAt(pos[0]);
    if (c == '{') return parseJsonObject(json, pos);
    if (c == '[') return parseJsonArray(json, pos);
    if (c == '"') return parseJsonString(json, pos);
    if (c == 't' || c == 'f') {
      if (json.startsWith("true", pos[0])) {
        pos[0] += 4;
        return true;
      }
      if (json.startsWith("false", pos[0])) {
        pos[0] += 5;
        return false;
      }
    }
    if (c == 'n') {
      if (json.startsWith("null", pos[0])) {
        pos[0] += 4;
        return null;
      }
    }
    if (c == '-' || Character.isDigit(c)) {
      int start = pos[0];
      if (c == '-') pos[0]++;
      while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
      if (pos[0] < json.length() && json.charAt(pos[0]) == '.') {
        pos[0]++;
        while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
      }
      String numStr = json.substring(start, pos[0]);
      try {
        if (numStr.contains(".")) {
          return Double.parseDouble(numStr);
        } else {
          return Long.parseLong(numStr);
        }
      } catch (NumberFormatException e) {
        return numStr;
      }
    }
    return null;
  }

  private static String parseJsonString(String json, int[] pos) {
    skipWhitespace(json, pos);
    if (pos[0] >= json.length() || json.charAt(pos[0]) != '"') {
      return "";
    }
    pos[0]++;
    StringBuilder sb = new StringBuilder();
    while (pos[0] < json.length() && json.charAt(pos[0]) != '"') {
      char c = json.charAt(pos[0]);
      if (c == '\\' && pos[0] + 1 < json.length()) {
        pos[0]++;
        char next = json.charAt(pos[0]);
        switch (next) {
          case '"': sb.append('"'); break;
          case '\\': sb.append('\\'); break;
          case '/': sb.append('/'); break;
          case 'b': sb.append('\b'); break;
          case 'f': sb.append('\f'); break;
          case 'n': sb.append('\n'); break;
          case 'r': sb.append('\r'); break;
          case 't': sb.append('\t'); break;
          default: sb.append(next);
        }
      } else {
        sb.append(c);
      }
      pos[0]++;
    }
    if (pos[0] < json.length() && json.charAt(pos[0]) == '"') {
      pos[0]++;
    }
    return sb.toString();
  }

  private static void skipWhitespace(String json, int[] pos) {
    while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) {
      pos[0]++;
    }
  }

  private static State parseStateJson(String json) {
    State s = new State();
    Map<String, Object> map = parseJson(json);
    Object wfId = map.get("workflowId");
    if (wfId instanceof String) {
      s.workflowId = (String) wfId;
    }
    return s;
  }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It deploys the "Bank Statement" pipeline to your Extend account.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   go run provision.go
//
// Generated by doc1 (template: bank-statement-extractor).

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
)

const (
	API     = "https://api.extend.ai"
	VERSION = "2026-02-09"
)

var (
	apiKey  string
	stateDir  string
	stateFile string
)

type State struct {
	WorkflowID string `json:"workflowId,omitempty"`
}

var state State

func init() {
	apiKey = os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
		os.Exit(1)
	}

	wd, err := os.Getwd()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
		os.Exit(1)
	}
	stateDir = filepath.Join(wd, ".extend")
	stateFile = filepath.Join(stateDir, "bank-statement-extractor.json")

	loadState()
}

func loadState() {
	data, err := os.ReadFile(stateFile)
	if err == nil {
		json.Unmarshal(data, &state)
	}
}

func saveState() {
	os.MkdirAll(stateDir, 0755)
	data, _ := json.MarshalIndent(state, "", "  ")
	os.WriteFile(stateFile, data, 0644)
}

func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
	var reqBody io.Reader
	if body != nil {
		bodyBytes, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewReader(bodyBytes)
	}

	req, err := http.NewRequest(method, API+pathName, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("x-extend-api-version", VERSION)
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var data map[string]interface{}
	json.Unmarshal(respBody, &data)

	if resp.StatusCode >= 400 {
		respStr := string(respBody)
		if len(respStr) > 300 {
			respStr = respStr[:300]
		}
		return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, respStr)
	}

	return data, nil
}

var workflow = map[string]interface{}{
	"name": "Bank Statement Processing Pipeline",
	"steps": []map[string]interface{}{
		{
			"name": "startTrigger1",
			"type": "TRIGGER",
			"next": []map[string]interface{}{
				{
					"step": "parse1",
				},
			},
		},
		{
			"name": "parse1",
			"type": "PARSE",
			"config": map[string]interface{}{
				"parseConfig": map[string]interface{}{
					"blockOptions": map[string]interface{}{
						"text": map[string]interface{}{
							"agentic": map[string]interface{}{
								"enabled": true,
							},
						},
					},
					"chunkingStrategy": map[string]interface{}{
						"type": "document",
					},
				},
			},
			"next": []map[string]interface{}{
				{
					"step": "extraction2",
				},
			},
		},
		{
			"name": "extraction2",
			"type": "EXTRACT",
			"config": map[string]interface{}{
				"extractorConfig": map[string]interface{}{
					"schema": map[string]interface{}{
						"type": "object",
						"properties": map[string]interface{}{
							"bank_name": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Name of the bank issuing the statement",
							},
							"statement_period_start": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Start date of the statement period in MM/DD/YYYY format",
							},
							"statement_period_end": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "End date of the statement period in MM/DD/YYYY format",
							},
							"account_number": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Customer account number",
							},
							"account_holder_names": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Names of account holders",
							},
							"account_type": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Type of account (e.g., High School Checking)",
							},
							"beginning_balance": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Starting balance in currency format",
							},
							"ending_balance": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Ending balance in currency format",
							},
							"total_deposits": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Total deposits and additions",
							},
							"total_withdrawals": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Combined total of all withdrawals",
							},
							"total_fees": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Total fees charged during period",
							},
							"transactions": map[string]interface{}{
								"type":        "array",
								"description": "List of transactions with date, description, and amount",
							},
						},
					},
				},
			},
		},
	},
}

func main() {
	workflowName := workflow["name"].(string)
	fmt.Printf("Deploying \"%s\"…\n", workflowName)

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{
			"steps": workflow["steps"],
		})
		if err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		// Try to find an existing workflow with the same name
		listPath := fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflowName))
		listResp, err := apiCall("GET", listPath, nil)
		if err == nil {
			var items []map[string]interface{}
			if data, ok := listResp["data"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			} else if data, ok := listResp["items"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			}

			for _, item := range items {
				if name, ok := item["name"].(string); ok && name == workflowName {
					if id, ok := item["id"].(string); ok {
						state.WorkflowID = id
						saveState()
						fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflowName, id)
						_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
							"steps": workflow["steps"],
						})
						if err != nil {
							fmt.Fprintf(os.Stderr, "%v\n", err)
							os.Exit(1)
						}
						break
					}
				}
			}
		}

		if state.WorkflowID == "" {
			created, err := apiCall("POST", "/workflows", workflow)
			if err != nil {
				fmt.Fprintf(os.Stderr, "%v\n", err)
				os.Exit(1)
			}

			var wfID string
			if id, ok := created["id"].(string); ok {
				wfID = id
			} else if wf, ok := created["workflow"].(map[string]interface{}); ok {
				if id, ok := wf["id"].(string); ok {
					wfID = id
				}
			}

			if wfID == "" {
				fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
				os.Exit(1)
			}

			state.WorkflowID = wfID
			saveState()
			fmt.Printf("+ created workflow (%s)\n", wfID)
		}
	}

	// Deploy the current draft as a new version (best-effort)
	apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})

	fmt.Println("\nDone. Run documents through it with:")
	fmt.Printf("  POST %s/workflow_runs  { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
	fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}

Frequently Asked Questions (FAQ)

Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand
If this implicates a significantly different extraction schema, use classify with bank-specific categories (e.g., `{ id: "chase", type: "Chase statement" }`, `{ id: "bofa", type: "Bank of America statement" }`, `{ id: "other" }`) before extracting—this tells the extractor which layout rules to apply.
For financial records, require ≥ 0.9 confidence on critical fields (account number, total balance, transaction amounts); for non-critical metadata (merchant description, transaction type), 0.75+ is acceptable.
Tags
BankingAccount StatementTransaction RecordsFinancial Document
About this template

This template processes bank statements to capture account holder information, checking account summaries, transaction histories, and balance details. It handles multi-section documents with account data, transaction tables, and customer service information from financial institutions.

Document formats
  • PDF
  • Images & Scans
Requirements
  • Long tables
  • Scanned documents

Relevant templates for Financial & Banking

  1. 01
    Driver's License ExtractorParse → Extract
    Extracts personal identification and licensing data from driver license documents.
    PDFImages & Scanswww.extend.ai/templates/driver-license-template
  2. 02
    Receipt ExtractorParse → Extract
    Extracts itemized sales, pricing, GST tax, and payment details from retail receipts.
    PDFImages & Scanswww.extend.ai/templates/receipt-parse-extract
  3. 03
    Bank Statement ExtractorParse → Extract
    Extracts account summaries, balances, deposits, NSF flags, and transaction details from bank statements.
    PDFImages & Scanswww.extend.ai/templates/bank-statement
  4. 04
    Vendor Invoice ExtractorParse → Extract
    Extracts charges, billing details, line items, totals, and due dates vendor invoices.
    PDFImages & Scanswww.extend.ai/templates/vendor-invoice
  5. 05
    Pay Stub ExtractorParse → Extract
    Extracts employee earnings, deductions, taxes, and net pay from pay stubs.
    PDFwww.extend.ai/templates/pay-stub
  6. 06
    Check ExtractorParse → Extract
    Extracts check details including payee, amount, date, and bank routing information.
    PDFImages & Scanswww.extend.ai/templates/check
  7. 07
    Wire Transfer Instructions ExtractorParse → Extract
    Extracts wire transfer procedures, contact info, and operational hours from banking guides.
    PDFWord / DOCXwww.extend.ai/templates/wire-transfer-instructions
  8. 08
    Onboarding Package ExtractorParse → Extract
    Extracts account summaries, transaction details, and balances from bank statements.
    PDFImages & Scanswww.extend.ai/templates/personal-bank-statement