Financial & BankingParse → Extract

Bank Statement Extractor

Extracts account summaries, balances, deposits, NSF flags, and transaction details from bank statements.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document494510142-Statement.pdf

Step-by-step

A bank statement is a periodic financial document issued by a financial institution that summarizes account activity, including opening and closing balances, deposits, withdrawals, fees, and a detailed transaction history for a specific account holder. This template takes in Bank Statement and outputs markdown (.md) capturing the statement's full text and layout structure, and JSON (.json) with structured account and transaction fields including account holder information, balance details, 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,"polygon":[{"x":55.0872,"y":141.012},{"x":126.96480000000001,"y":141.0552},{"x":126.95760000000001,"y":150.7104},{"x":55.0872,"y":150.66}],"pageWidth…changed
metadata.account_holder_name.logprobsConfidencenullchanged
metadata.account_holder_name.ocrConfidence0.992changed
metadata.account_number.citations[{"page":1,"polygon":[{"x":361.08,"y":60.0192},{"x":422.5824,"y":60.0264},{"x":422.5824,"y":68.3928},{"x":361.08,"y":68.3856}],"pageWidth":612,"pageHeight":792,…changed
metadata.account_number.logprobsConfidencenullchanged
metadata.account_number.ocrConfidence0.992changed
metadata.bank_name.citations[{"page":1,"polygon":[{"x":58.2768,"y":54.309599999999996},{"x":157.7736,"y":54.2016},{"x":157.7808,"y":62.906400000000005},{"x":58.291199999999996,"y":62.9784}…changed
metadata.bank_name.logprobsConfidencenullchanged
metadata.bank_name.ocrConfidence0.993changed
metadata.beginning_balance.citations[{"page":1,"polygon":[{"x":38.9736,"y":413.532},{"x":121.9248,"y":413.5608},{"x":121.9248,"y":424.46880000000004},{"x":38.9664,"y":424.43999999999994}],"pageWid…changed
metadata.beginning_balance.logprobsConfidencenullchanged
metadata.beginning_balance.ocrConfidence0.995changed
metadata.ending_balance.citations[{"page":1,"polygon":[{"x":39.0168,"y":480.2832},{"x":108.4536,"y":480.3768},{"x":108.4392,"y":490.6656},{"x":39.002399999999994,"y":490.5792}],"pageWidth":612,…changed
metadata.ending_balance.logprobsConfidencenullchanged
metadata.ending_balance.ocrConfidence0.995changed
metadata.nsf_flag.citations[{"page":1,"polygon":[{"x":39.0168,"y":480.2832},{"x":108.4536,"y":480.3768},{"x":108.4392,"y":490.6656},{"x":39.002399999999994,"y":490.5792}],"pageWidth":612,…changed
metadata.nsf_flag.logprobsConfidencenullchanged
metadata.nsf_flag.ocrConfidence0.996changed
metadata.statement_period_end_date.citations[{"page":1,"polygon":[{"x":359.8488,"y":46.360800000000005},{"x":519.7968000000001,"y":46.3824},{"x":519.7968000000001,"y":55.987199999999994},{"x":359.8488,"y"…changed
metadata.statement_period_end_date.logprobsConfidencenullchanged
metadata.statement_period_end_date.ocrConfidence0.989changed
metadata.statement_period_start_date.citations[{"page":1,"polygon":[{"x":359.8488,"y":46.360800000000005},{"x":519.7968000000001,"y":46.3824},{"x":519.7968000000001,"y":55.987199999999994},{"x":359.8488,"y"…changed
metadata.statement_period_start_date.logprobsConfidencenullchanged
metadata.statement_period_start_date.ocrConfidence0.99changed
metadata.total_deposits.citations[{"page":1,"polygon":[{"x":39.1104,"y":426.96},{"x":133.1568,"y":426.9168},{"x":133.164,"y":437.6592},{"x":39.1104,"y":437.6952}],"pageWidth":612,"pageHeight":7…changed
metadata.total_deposits.logprobsConfidencenullchanged
metadata.total_deposits.ocrConfidence0.992changed
metadata.total_fees.citations[{"page":1,"polygon":[{"x":39.1032,"y":467.3016},{"x":60.4728,"y":467.4384},{"x":60.4152,"y":476.3952},{"x":39.0456,"y":476.2584}],"pageWidth":612,"pageHeight":…changed
metadata.total_fees.logprobsConfidencenullchanged
metadata.total_fees.ocrConfidence0.995changed
metadata.total_withdrawals.citations[{"page":1,"polygon":[{"x":39.139199999999995,"y":439.7832},{"x":166.1472,"y":439.86240000000004},{"x":166.14000000000001,"y":450.3816},{"x":39.132,"y":450.3024…changed
metadata.total_withdrawals.logprobsConfidencenullchanged
metadata.total_withdrawals.ocrConfidence0.989changed
metadata.transactions.citations[]changed
metadata.transactions.logprobsConfidencenullchanged
metadata.transactions.ocrConfidence0.655changed
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,"polygon":[{"x":79.8552,"y":563.2776},{"x":115.6968,"y":563.4431999999999},{"x":115.65360000000001,"y":572.4504000000001},{"x":79.8192,"y":572.2848}]…changed
metadata.transactions[0].description.logprobsConfidencenullchanged
metadata.transactions[0].description.ocrConfidence0.985changed
metadata.transactions[0].logprobsConfidencenullchanged
metadata.transactions[0].nsf_related.logprobsConfidencenullchanged
metadata.transactions[0].ocrConfidence0.985changed
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.994changed
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,"polygon":[{"x":79.14959999999999,"y":722.4696},{"x":348.89759999999995,"y":722.5487999999999},{"x":348.89759999999995,"y":732.8376000000001},{"x":79…changed
metadata.transactions[10].description.logprobsConfidencenullchanged
metadata.transactions[10].description.ocrConfidence0.988changed
metadata.transactions[10].logprobsConfidencenullchanged
metadata.transactions[10].nsf_related.logprobsConfidencenullchanged
metadata.transactions[10].ocrConfidence0.988changed
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,"polygon":[{"x":33.7176,"y":130.0464},{"x":57.5568,"y":130.1184},{"x":57.5352,"y":139.0032},{"x":33.6888,"y":138.9312}],"pageWidth":612,"pageHeight":…changed
metadata.transactions[11].description.logprobsConfidencenullchanged
metadata.transactions[11].description.ocrConfidence0.985changed
metadata.transactions[11].logprobsConfidencenullchanged
metadata.transactions[11].nsf_related.logprobsConfidencenullchanged
metadata.transactions[11].ocrConfidence0.985changed
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.993changed
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,"polygon":[{"x":33.588,"y":142.4016},{"x":57.5064,"y":142.4448},{"x":57.492,"y":151.3944},{"x":33.5736,"y":151.3512}],"pageWidth":612,"pageHeight":79…changed
metadata.transactions[12].description.logprobsConfidencenullchanged
metadata.transactions[12].description.ocrConfidence0.9changed
metadata.transactions[12].logprobsConfidencenullchanged
metadata.transactions[12].nsf_related.logprobsConfidencenullchanged
metadata.transactions[12].ocrConfidence0.9changed
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.993changed
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,"polygon":[{"x":77.1048,"y":154.62},{"x":137.1672,"y":154.6848},{"x":137.16,"y":164.1384},{"x":77.0976,"y":164.0736}],"pageWidth":612,"pageHeight":79…changed
metadata.transactions[13].description.logprobsConfidencenullchanged
metadata.transactions[13].description.ocrConfidence0.916changed
metadata.transactions[13].logprobsConfidencenullchanged
metadata.transactions[13].nsf_related.logprobsConfidencenullchanged
metadata.transactions[13].ocrConfidence0.916changed
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.992changed
metadata.transactions[14].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[14].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[14].citations[{"page":2,"polygon":[{"x":33.6096,"y":167.148},{"x":57.1248,"y":167.1624},{"x":57.1176,"y":176.0616},{"x":33.6024,"y":176.0472}],"pageWidth":612,"pageHeight":7…changed
metadata.transactions[14].description.logprobsConfidencenullchanged
metadata.transactions[14].description.ocrConfidence0.964changed
metadata.transactions[14].logprobsConfidencenullchanged
metadata.transactions[14].nsf_related.logprobsConfidencenullchanged
metadata.transactions[14].ocrConfidence0.964changed
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.992changed
metadata.transactions[15].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[15].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[15].citations[{"page":2,"polygon":[{"x":77.1624,"y":178.74},{"x":135.1296,"y":178.7184},{"x":135.1368,"y":188.7984},{"x":77.1624,"y":188.82}],"pageWidth":612,"pageHeight":79…changed
metadata.transactions[15].description.logprobsConfidencenullchanged
metadata.transactions[15].description.ocrConfidence0.881changed
metadata.transactions[15].logprobsConfidencenullchanged
metadata.transactions[15].nsf_related.logprobsConfidencenullchanged
metadata.transactions[15].ocrConfidence0.881changed
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.994changed
metadata.transactions[16].amount.logprobsConfidencenullchanged
metadata.transactions[16].amount.ocrConfidence0.655changed
metadata.transactions[16].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[16].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[16].citations[{"page":2,"polygon":[{"x":33.552,"y":200.49120000000002},{"x":57.002399999999994,"y":200.4984},{"x":57.002399999999994,"y":209.5416},{"x":33.544799999999995,"y…changed
metadata.transactions[16].description.logprobsConfidencenullchanged
metadata.transactions[16].description.ocrConfidence0.985changed
metadata.transactions[16].logprobsConfidencenullchanged
metadata.transactions[16].nsf_related.logprobsConfidencenullchanged
metadata.transactions[16].ocrConfidence0.655changed
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.994changed
metadata.transactions[17].amount.logprobsConfidencenullchanged
metadata.transactions[17].amount.ocrConfidence0.992changed
metadata.transactions[17].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[17].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[17].citations[{"page":2,"polygon":[{"x":33.6744,"y":212.88240000000002},{"x":56.937599999999996,"y":212.8896},{"x":56.937599999999996,"y":221.8536},{"x":33.6744,"y":221.8464…changed
metadata.transactions[17].description.logprobsConfidencenullchanged
metadata.transactions[17].description.ocrConfidence0.964changed
metadata.transactions[17].logprobsConfidencenullchanged
metadata.transactions[17].nsf_related.logprobsConfidencenullchanged
metadata.transactions[17].ocrConfidence0.964changed
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.993changed
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,"polygon":[{"x":33.580799999999996,"y":225.3672},{"x":55.872,"y":225.37439999999998},{"x":55.872,"y":234.1944},{"x":33.5736,"y":234.1944}],"pageWidth…changed
metadata.transactions[18].description.logprobsConfidencenullchanged
metadata.transactions[18].description.ocrConfidence0.985changed
metadata.transactions[18].logprobsConfidencenullchanged
metadata.transactions[18].nsf_related.logprobsConfidencenullchanged
metadata.transactions[18].ocrConfidence0.985changed
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.993changed
metadata.transactions[19].amount.logprobsConfidencenullchanged
metadata.transactions[19].amount.ocrConfidence0.655changed
metadata.transactions[19].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[19].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[19].citations[{"page":2,"polygon":[{"x":33.4368,"y":246.8016},{"x":55.9584,"y":246.8376},{"x":55.944,"y":255.7728},{"x":33.4224,"y":255.7368}],"pageWidth":612,"pageHeight":7…changed
metadata.transactions[19].description.logprobsConfidencenullchanged
metadata.transactions[19].description.ocrConfidence0.975changed
metadata.transactions[19].logprobsConfidencenullchanged
metadata.transactions[19].nsf_related.logprobsConfidencenullchanged
metadata.transactions[19].ocrConfidence0.655changed
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.994changed
metadata.transactions[1].amount.logprobsConfidencenullchanged
metadata.transactions[1].amount.ocrConfidence0.996changed
metadata.transactions[1].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[1].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[1].citations[{"page":1,"polygon":[{"x":79.30799999999999,"y":574.9488},{"x":330.6168,"y":575.0064},{"x":330.6168,"y":585.9288},{"x":79.30799999999999,"y":585.864}],"pageWid…changed
metadata.transactions[1].description.logprobsConfidencenullchanged
metadata.transactions[1].description.ocrConfidence0.989changed
metadata.transactions[1].logprobsConfidencenullchanged
metadata.transactions[1].nsf_related.logprobsConfidencenullchanged
metadata.transactions[1].ocrConfidence0.989changed
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.994changed
metadata.transactions[20].amount.logprobsConfidencenullchanged
metadata.transactions[20].amount.ocrConfidence0.983changed
metadata.transactions[20].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[20].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[20].citations[{"page":2,"polygon":[{"x":76.7448,"y":267.4584},{"x":329.9832,"y":267.3576},{"x":329.99039999999997,"y":278.0424},{"x":76.75200000000001,"y":278.14320000000004…changed
metadata.transactions[20].description.logprobsConfidencenullchanged
metadata.transactions[20].description.ocrConfidence0.956changed
metadata.transactions[20].logprobsConfidencenullchanged
metadata.transactions[20].nsf_related.logprobsConfidencenullchanged
metadata.transactions[20].ocrConfidence0.956changed
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.983changed
metadata.transactions[21].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[21].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[21].citations[{"page":2,"polygon":[{"x":33.696000000000005,"y":280.7136},{"x":57.132,"y":280.84319999999997},{"x":57.081599999999995,"y":289.7496},{"x":33.6456,"y":289.62}],…changed
metadata.transactions[21].description.logprobsConfidencenullchanged
metadata.transactions[21].description.ocrConfidence0.964changed
metadata.transactions[21].logprobsConfidencenullchanged
metadata.transactions[21].nsf_related.logprobsConfidencenullchanged
metadata.transactions[21].ocrConfidence0.964changed
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,"polygon":[{"x":76.96079999999999,"y":292.7088},{"x":137.6208,"y":292.9176},{"x":137.592,"y":302.6664},{"x":76.9248,"y":302.4576}],"pageWidth":612,"p…changed
metadata.transactions[22].description.logprobsConfidencenullchanged
metadata.transactions[22].description.ocrConfidence0.916changed
metadata.transactions[22].logprobsConfidencenullchanged
metadata.transactions[22].nsf_related.logprobsConfidencenullchanged
metadata.transactions[22].ocrConfidence0.916changed
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,"polygon":[{"x":33.7032,"y":304.99920000000003},{"x":57.0312,"y":305.0712},{"x":57.002399999999994,"y":314.0784},{"x":33.6744,"y":314.0064}],"pageWid…changed
metadata.transactions[23].description.logprobsConfidencenullchanged
metadata.transactions[23].description.ocrConfidence0.916changed
metadata.transactions[23].logprobsConfidencenullchanged
metadata.transactions[23].nsf_related.logprobsConfidencenullchanged
metadata.transactions[23].ocrConfidence0.916changed
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.993changed
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,"polygon":[{"x":33.732,"y":317.34},{"x":57.06,"y":317.3544},{"x":57.0528,"y":326.41200000000003},{"x":33.7248,"y":326.3904}],"pageWidth":612,"pageHei…changed
metadata.transactions[24].description.logprobsConfidencenullchanged
metadata.transactions[24].description.ocrConfidence0.916changed
metadata.transactions[24].logprobsConfidencenullchanged
metadata.transactions[24].nsf_related.logprobsConfidencenullchanged
metadata.transactions[24].ocrConfidence0.916changed
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.993changed
metadata.transactions[25].amount.logprobsConfidencenullchanged
metadata.transactions[25].amount.ocrConfidence0.992changed
metadata.transactions[25].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[25].balance_after_transaction.ocrConfidence0.967changed
metadata.transactions[25].citations[{"page":2,"polygon":[{"x":76.7736,"y":328.752},{"x":351.37440000000004,"y":328.8528},{"x":351.37440000000004,"y":339.58799999999997},{"x":76.7664,"y":339.4872}…changed
metadata.transactions[25].description.logprobsConfidencenullchanged
metadata.transactions[25].description.ocrConfidence0.959changed
metadata.transactions[25].logprobsConfidencenullchanged
metadata.transactions[25].nsf_related.logprobsConfidencenullchanged
metadata.transactions[25].ocrConfidence0.959changed
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.995changed
metadata.transactions[26].citations[{"page":2,"polygon":[{"x":33.544799999999995,"y":379.08720000000005},{"x":57.4344,"y":379.1088},{"x":57.42,"y":388.11600000000004},{"x":33.5376,"y":388.0944}],…changed
metadata.transactions[26].description.logprobsConfidencenullchanged
metadata.transactions[26].description.ocrConfidence0.984changed
metadata.transactions[26].logprobsConfidencenullchanged
metadata.transactions[26].nsf_related.logprobsConfidencenullchanged
metadata.transactions[26].ocrConfidence0.984changed
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.993changed
metadata.transactions[27].amount.logprobsConfidencenullchanged
metadata.transactions[27].amount.ocrConfidence0.994changed
metadata.transactions[27].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[27].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[27].citations[{"page":2,"polygon":[{"x":33.5376,"y":400.92479999999995},{"x":57.4632,"y":400.9392},{"x":57.456,"y":410.09040000000005},{"x":33.5376,"y":410.076}],"pageWidth"…changed
metadata.transactions[27].description.logprobsConfidencenullchanged
metadata.transactions[27].description.ocrConfidence0.881changed
metadata.transactions[27].logprobsConfidencenullchanged
metadata.transactions[27].nsf_related.logprobsConfidencenullchanged
metadata.transactions[27].ocrConfidence0.881changed
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.994changed
metadata.transactions[28].amount.logprobsConfidencenullchanged
metadata.transactions[28].amount.ocrConfidence0.983changed
metadata.transactions[28].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[28].balance_after_transaction.ocrConfidence0.995changed
metadata.transactions[28].citations[{"page":2,"polygon":[{"x":33.3576,"y":422.5608},{"x":57.355199999999996,"y":422.48879999999997},{"x":57.3768,"y":431.73359999999997},{"x":33.3864,"y":431.8056}…changed
metadata.transactions[28].description.logprobsConfidencenullchanged
metadata.transactions[28].description.ocrConfidence0.881changed
metadata.transactions[28].logprobsConfidencenullchanged
metadata.transactions[28].nsf_related.logprobsConfidencenullchanged
metadata.transactions[28].ocrConfidence0.881changed
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.994changed
metadata.transactions[29].amount.logprobsConfidencenullchanged
metadata.transactions[29].amount.ocrConfidence0.985changed
metadata.transactions[29].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[29].balance_after_transaction.ocrConfidence0.992changed
metadata.transactions[29].citations[{"page":2,"polygon":[{"x":76.7088,"y":443.07360000000006},{"x":368.6112,"y":443.2464},{"x":368.60400000000004,"y":453.9384},{"x":76.7088,"y":453.77279999999996…changed
metadata.transactions[29].description.logprobsConfidencenullchanged
metadata.transactions[29].description.ocrConfidence0.83changed
metadata.transactions[29].logprobsConfidencenullchanged
metadata.transactions[29].nsf_related.logprobsConfidencenullchanged
metadata.transactions[29].ocrConfidence0.83changed
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,"polygon":[{"x":79.4088,"y":587.3328},{"x":379.2312,"y":587.2320000000001},{"x":379.2312,"y":597.8736},{"x":79.4088,"y":597.9744}],"pageWidth":612,"p…changed
metadata.transactions[2].description.logprobsConfidencenullchanged
metadata.transactions[2].description.ocrConfidence0.977changed
metadata.transactions[2].logprobsConfidencenullchanged
metadata.transactions[2].nsf_related.logprobsConfidencenullchanged
metadata.transactions[2].ocrConfidence0.977changed
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,"polygon":[{"x":33.588,"y":475.2},{"x":57.355199999999996,"y":475.2},{"x":57.355199999999996,"y":484.3008},{"x":33.588,"y":484.3008}],"pageWidth":612…changed
metadata.transactions[30].description.logprobsConfidencenullchanged
metadata.transactions[30].description.ocrConfidence0.964changed
metadata.transactions[30].logprobsConfidencenullchanged
metadata.transactions[30].nsf_related.logprobsConfidencenullchanged
metadata.transactions[30].ocrConfidence0.964changed
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.994changed
metadata.transactions[31].amount.logprobsConfidencenullchanged
metadata.transactions[31].amount.ocrConfidence0.993changed
metadata.transactions[31].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[31].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[31].citations[{"page":2,"polygon":[{"x":76.7592,"y":486.5184},{"x":184.0608,"y":486.51120000000003},{"x":184.0608,"y":496.8216},{"x":76.7592,"y":496.8288}],"pageWidth":612,"…changed
metadata.transactions[31].description.logprobsConfidencenullchanged
metadata.transactions[31].description.ocrConfidence0.982changed
metadata.transactions[31].logprobsConfidencenullchanged
metadata.transactions[31].nsf_related.logprobsConfidencenullchanged
metadata.transactions[31].ocrConfidence0.982changed
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,"polygon":[{"x":33.4584,"y":499.41360000000003},{"x":57.0672,"y":499.4064},{"x":57.0672,"y":508.6944},{"x":33.4584,"y":508.7016}],"pageWidth":612,"pa…changed
metadata.transactions[32].description.logprobsConfidencenullchanged
metadata.transactions[32].description.ocrConfidence0.881changed
metadata.transactions[32].logprobsConfidencenullchanged
metadata.transactions[32].nsf_related.logprobsConfidencenullchanged
metadata.transactions[32].ocrConfidence0.881changed
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.994changed
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,"polygon":[{"x":33.4584,"y":520.8912},{"x":57.132,"y":520.9056},{"x":57.1248,"y":530.0568000000001},{"x":33.4512,"y":530.0424}],"pageWidth":612,"page…changed
metadata.transactions[33].description.logprobsConfidencenullchanged
metadata.transactions[33].description.ocrConfidence0.977changed
metadata.transactions[33].logprobsConfidencenullchanged
metadata.transactions[33].nsf_related.logprobsConfidencenullchanged
metadata.transactions[33].ocrConfidence0.977changed
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.993changed
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,"polygon":[{"x":33.544799999999995,"y":542.484},{"x":56.9088,"y":542.556},{"x":56.88,"y":551.6496000000001},{"x":33.5232,"y":551.5848}],"pageWidth":6…changed
metadata.transactions[34].description.logprobsConfidencenullchanged
metadata.transactions[34].description.ocrConfidence0.985changed
metadata.transactions[34].logprobsConfidencenullchanged
metadata.transactions[34].nsf_related.logprobsConfidencenullchanged
metadata.transactions[34].ocrConfidence0.985changed
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.993changed
metadata.transactions[35].amount.logprobsConfidencenullchanged
metadata.transactions[35].amount.ocrConfidence0.996changed
metadata.transactions[35].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[35].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[35].citations[{"page":2,"polygon":[{"x":33.5736,"y":554.7528},{"x":56.9592,"y":554.8464},{"x":56.923199999999994,"y":563.9976},{"x":33.5376,"y":563.904}],"pageWidth":612,"pa…changed
metadata.transactions[35].description.logprobsConfidencenullchanged
metadata.transactions[35].description.ocrConfidence0.985changed
metadata.transactions[35].logprobsConfidencenullchanged
metadata.transactions[35].nsf_related.logprobsConfidencenullchanged
metadata.transactions[35].ocrConfidence0.985changed
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.993changed
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,"polygon":[{"x":33.6168,"y":567.288},{"x":56.8224,"y":567.3672},{"x":56.7936,"y":576.2520000000001},{"x":33.588,"y":576.1727999999999}],"pageWidth":6…changed
metadata.transactions[36].description.logprobsConfidencenullchanged
metadata.transactions[36].description.ocrConfidence0.985changed
metadata.transactions[36].logprobsConfidencenullchanged
metadata.transactions[36].nsf_related.logprobsConfidencenullchanged
metadata.transactions[36].ocrConfidence0.985changed
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.993changed
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,"polygon":[{"x":76.87440000000001,"y":578.5632},{"x":138.6,"y":578.8872},{"x":138.5496,"y":588.7944},{"x":76.824,"y":588.4703999999999}],"pageWidth":…changed
metadata.transactions[37].description.logprobsConfidencenullchanged
metadata.transactions[37].description.ocrConfidence0.837changed
metadata.transactions[37].logprobsConfidencenullchanged
metadata.transactions[37].nsf_related.logprobsConfidencenullchanged
metadata.transactions[37].ocrConfidence0.837changed
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.993changed
metadata.transactions[38].amount.logprobsConfidencenullchanged
metadata.transactions[38].amount.ocrConfidence0.992changed
metadata.transactions[38].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[38].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[38].citations[{"page":2,"polygon":[{"x":33.588,"y":591.6168},{"x":57.1248,"y":591.7031999999999},{"x":57.088800000000006,"y":600.6384},{"x":33.552,"y":600.5519999999999}],"p…changed
metadata.transactions[38].description.logprobsConfidencenullchanged
metadata.transactions[38].description.ocrConfidence0.964changed
metadata.transactions[38].logprobsConfidencenullchanged
metadata.transactions[38].nsf_related.logprobsConfidencenullchanged
metadata.transactions[38].ocrConfidence0.964changed
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.983changed
metadata.transactions[39].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[39].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[39].citations[{"page":2,"polygon":[{"x":33.6024,"y":603.9288},{"x":57.002399999999994,"y":603.972},{"x":56.988,"y":612.9864},{"x":33.580799999999996,"y":612.936}],"pageWidth…changed
metadata.transactions[39].description.logprobsConfidencenullchanged
metadata.transactions[39].description.ocrConfidence0.964changed
metadata.transactions[39].logprobsConfidencenullchanged
metadata.transactions[39].nsf_related.logprobsConfidencenullchanged
metadata.transactions[39].ocrConfidence0.964changed
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.994changed
metadata.transactions[3].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[3].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[3].citations[{"page":1,"polygon":[{"x":79.47359999999999,"y":609.0840000000001},{"x":156.2544,"y":609.2928},{"x":156.2328,"y":619.2575999999999},{"x":79.452,"y":619.0487999…changed
metadata.transactions[3].description.logprobsConfidencenullchanged
metadata.transactions[3].description.ocrConfidence0.988changed
metadata.transactions[3].logprobsConfidencenullchanged
metadata.transactions[3].nsf_related.logprobsConfidencenullchanged
metadata.transactions[3].ocrConfidence0.988changed
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.995changed
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,"polygon":[{"x":33.6168,"y":616.2983999999999},{"x":57.0384,"y":616.356},{"x":57.0168,"y":625.3704},{"x":33.5952,"y":625.3128}],"pageWidth":612,"page…changed
metadata.transactions[40].description.logprobsConfidencenullchanged
metadata.transactions[40].description.ocrConfidence0.985changed
metadata.transactions[40].logprobsConfidencenullchanged
metadata.transactions[40].nsf_related.logprobsConfidencenullchanged
metadata.transactions[40].ocrConfidence0.985changed
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.993changed
metadata.transactions[41].amount.logprobsConfidencenullchanged
metadata.transactions[41].amount.ocrConfidence0.993changed
metadata.transactions[41].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[41].balance_after_transaction.ocrConfidence0.998changed
metadata.transactions[41].citations[{"page":2,"polygon":[{"x":76.8816,"y":627.66},{"x":308.8152,"y":627.5304},{"x":308.8224,"y":638.2008},{"x":76.8888,"y":638.3304}],"pageWidth":612,"pageHeight":…changed
metadata.transactions[41].description.logprobsConfidencenullchanged
metadata.transactions[41].description.ocrConfidence0.989changed
metadata.transactions[41].logprobsConfidencenullchanged
metadata.transactions[41].nsf_related.logprobsConfidencenullchanged
metadata.transactions[41].ocrConfidence0.989changed
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.994changed
metadata.transactions[42].amount.logprobsConfidencenullchanged
metadata.transactions[42].amount.ocrConfidence0.993changed
metadata.transactions[42].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[42].balance_after_transaction.ocrConfidence0.996changed
metadata.transactions[42].citations[{"page":2,"polygon":[{"x":76.84559999999999,"y":639.972},{"x":138.7584,"y":640.3824},{"x":138.6936,"y":650.3544},{"x":76.7808,"y":649.944}],"pageWidth":612,"pa…changed
metadata.transactions[42].description.logprobsConfidencenullchanged
metadata.transactions[42].description.ocrConfidence0.973changed
metadata.transactions[42].logprobsConfidencenullchanged
metadata.transactions[42].nsf_related.logprobsConfidencenullchanged
metadata.transactions[42].ocrConfidence0.973changed
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.993changed
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,"polygon":[{"x":79.2648,"y":621.5904},{"x":140.724,"y":621.9072000000001},{"x":140.6736,"y":631.368},{"x":79.21440000000001,"y":631.1880000000001}],"…changed
metadata.transactions[4].description.logprobsConfidencenullchanged
metadata.transactions[4].description.ocrConfidence0.988changed
metadata.transactions[4].logprobsConfidencenullchanged
metadata.transactions[4].nsf_related.logprobsConfidencenullchanged
metadata.transactions[4].ocrConfidence0.988changed
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,"polygon":[{"x":79.2,"y":633.4632},{"x":140.54399999999998,"y":633.6863999999999},{"x":140.508,"y":643.3272000000001},{"x":79.16399999999999,"y":643.…changed
metadata.transactions[5].description.logprobsConfidencenullchanged
metadata.transactions[5].description.ocrConfidence0.988changed
metadata.transactions[5].logprobsConfidencenullchanged
metadata.transactions[5].nsf_related.logprobsConfidencenullchanged
metadata.transactions[5].ocrConfidence0.988changed
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,"polygon":[{"x":79.128,"y":645.8976},{"x":140.57999999999998,"y":646.0992},{"x":140.5512,"y":655.5312},{"x":79.0992,"y":655.3296}],"pageWidth":612,"p…changed
metadata.transactions[6].description.logprobsConfidencenullchanged
metadata.transactions[6].description.ocrConfidence0.977changed
metadata.transactions[6].logprobsConfidencenullchanged
metadata.transactions[6].nsf_related.logprobsConfidencenullchanged
metadata.transactions[6].ocrConfidence0.977changed
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,"polygon":[{"x":79.3728,"y":667.3176},{"x":140.616,"y":667.6848},{"x":140.5584,"y":677.1456},{"x":79.31519999999999,"y":676.7783999999999}],"pageWidt…changed
metadata.transactions[7].description.logprobsConfidencenullchanged
metadata.transactions[7].description.ocrConfidence0.977changed
metadata.transactions[7].logprobsConfidencenullchanged
metadata.transactions[7].nsf_related.logprobsConfidencenullchanged
metadata.transactions[7].ocrConfidence0.977changed
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.998changed
metadata.transactions[8].balance_after_transaction.logprobsConfidencenullchanged
metadata.transactions[8].balance_after_transaction.ocrConfidence0.997changed
metadata.transactions[8].citations[{"page":1,"polygon":[{"x":79.4016,"y":689.0111999999999},{"x":137.7648,"y":689.1192},{"x":137.7504,"y":698.8464},{"x":79.38,"y":698.7384000000001}],"pageWidth"…changed
metadata.transactions[8].description.logprobsConfidencenullchanged
metadata.transactions[8].description.ocrConfidence0.931changed
metadata.transactions[8].logprobsConfidencenullchanged
metadata.transactions[8].nsf_related.logprobsConfidencenullchanged
metadata.transactions[8].ocrConfidence0.931changed
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.994changed
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,"polygon":[{"x":79.30799999999999,"y":710.568},{"x":140.796,"y":710.8704},{"x":140.7456,"y":720.144},{"x":79.2864,"y":719.8344}],"pageWidth":612,"pag…changed
metadata.transactions[9].description.logprobsConfidencenullchanged
metadata.transactions[9].description.ocrConfidence0.988changed
metadata.transactions[9].logprobsConfidencenullchanged
metadata.transactions[9].nsf_related.logprobsConfidencenullchanged
metadata.transactions[9].ocrConfidence0.988changed
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.bank_name"JPMorgan Chase Bank, N.A."changed
value.beginning_balance.amount438.59changed
value.beginning_balance.iso_4217_currency_code"USD"changed
value.ending_balance.amount459.25changed
value.ending_balance.iso_4217_currency_code"USD"changed
value.nsf_flagfalsechanged
value.statement_period_end_date"2021-01-25"changed
value.statement_period_start_date"2020-12-22"changed
value.total_deposits.amount3098.99changed
value.total_deposits.iso_4217_currency_code"USD"changed
value.total_fees.amount25changed
value.total_fees.iso_4217_currency_code"USD"changed
value.total_withdrawals.amount3053.33changed
value.total_withdrawals.iso_4217_currency_code"USD"changed
value.transactions[{"amount":-27.9,"description":"Discover E-Payment 6930 Web ID: 2510020270","nsf_related":false,"transaction_date":"2020-12-24","transaction_type":"payment","ba…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 parses bank statements into markdown, then extracts structured fields including account holder name, account number, balances, transaction history, and fee summaries. It handles multi-page statements with complex transaction tables and returns fully typed JSON ready for downstream processing (reconciliation, reporting, fraud detection).

## When to use this

- **Automated bank statement ingestion** for accounting systems, fintech platforms, or loan underwriting workflows
- **Transaction categorization & reporting** — extract spending patterns, fee analysis, or cash flow summaries
- **Fraud detection & compliance** — identify NSF flags, unusual wire transfers, or high-velocity transactions
- **Customer onboarding** — verify account ownership and financial position from statement PDFs
- **Multi-bank reconciliation** — normalize statements from different banks into a consistent schema

## Processor pipeline

### Step 1: Parse (`parse_performance` with agentic OCR)
**Purpose:** Convert the bank statement PDF into structured markdown with high-fidelity text extraction.

**Key config:**
- `engine: "parse_performance"` — balances speed (~2–3 sec) with accuracy for dense financial tables
- `blockOptions.text.agentic.enabled: true` — enables AI-powered OCR for handwriting, logos, and table structure
- `chunkingStrategy.type: "document"` — keeps transaction table intact as one semantic unit

**Why this config:** Bank statements have dense transaction tables with aligned columns and precise formatting. Agentic OCR understands table semantics and preserves row/column relationships, whereas naive OCR would scramble amounts and descriptions. Document-level chunking prevents splitting mid-table.

### Step 2: Extract (structured fields)
**Purpose:** Pull account metadata, balances, and all transactions into a strongly-typed object.

**Key config:**
- `baseProcessor: "extraction_performance"` — uses semantic understanding to map loosely-formatted transaction descriptions to standardized transaction types (payment, deposit, withdrawal, transfer, fee)
- Zod schema with `z.array()` for transactions — handles variable-length transaction lists (40+ txns common)
- `extendCurrency()` helper — parses amounts like "$1,234.56" or "1234.56" into `{ amount: number, iso_4217_currency_code: string }`
- `extendDate()` helper — normalizes dates (12/25/2020, Dec 25, 2020, etc.) to ISO 8601

**Why this config:** Financial data must be accurate to the cent and must reconcile. The extraction processor is trained on 1000+ real bank statements and understands abbreviations (ACH, PPP, Wire Reversal, etc.) that naive regex would miss. Currency and date helpers prevent common parsing errors.

## TypeScript implementation

```typescript
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 bank statement schema using Zod
const transactionSchema = z.object({
  transaction_date: extendDate().describe(
    "Date of the transaction in ISO 8601 format (YYYY-MM-DD). Extract from the date column of the statement."
  ),
  description: z.string().nullable().describe(
    "Full transaction description, including merchant name, location, reference number, and transaction ID if present. Must capture the complete merchant or counterparty identifier."
  ),
  amount: z.number().nullable().describe(
    "Transaction amount as a signed number. Positive for deposits, negative for withdrawals/payments/fees."
  ),
  transaction_type: z.enum([
    "payment",
    "deposit",
    "withdrawal",
    "transfer",
    "fee",
    "other"
  ]).describe(
    "Categorize the transaction: 'payment' (bill payment, check, card payment), 'deposit' (paycheck, transfer in), 'withdrawal' (ATM, debit card purchase), 'transfer' (inter-account), 'fee' (service charge), 'other' if unclear."
  ),
  balance_after_transaction: z.number().nullable().describe(
    "Running balance after this transaction. Used to validate extraction accuracy and detect NSF conditions."
  ),
  nsf_related: z.boolean().describe(
    "True if this transaction is marked as NSF (non-sufficient funds) or if the balance after would be negative, indicating a potential overdraft condition."
  ),
});

const bankStatementSchema = z.object({
  bank_name: z.string().nullable().describe(
    "Full legal name of the bank (e.g., 'JPMorgan Chase Bank, N.A.'). Extract from the header or logo area."
  ),
  account_holder_name: z.string().nullable().describe(
    "Primary account holder name(s) as printed on the statement. Include all names separated by 'AND' or 'OR' as shown."
  ),
  account_number: z.string().nullable().describe(
    "Account number, usually masked as '****1234' or shown in full. Extract exactly as displayed."
  ),
  statement_period_start_date: extendDate().describe(
    "First day of the statement period in ISO 8601 format."
  ),
  statement_period_end_date: extendDate().describe(
    "Last day of the statement period in ISO 8601 format."
  ),
  beginning_balance: extendCurrency().describe(
    "Opening balance at the start of the statement period, including currency code."
  ),
  ending_balance: extendCurrency().describe(
    "Closing balance at the end of the statement period. Used to reconcile all transactions."
  ),
  total_deposits: extendCurrency().describe(
    "Sum of all credits/deposits during the period. Verify this matches sum of positive transactions."
  ),
  total_withdrawals: extendCurrency().describe(
    "Sum of all debits/withdrawals during the period (as a positive number). Verify this matches sum of negative transactions in absolute value."
  ),
  total_fees: extendCurrency().describe(
    "Total fees charged during the period. Extract from the 'Fees' line if present, or sum all fee transactions."
  ),
  transactions: z.array(transactionSchema).describe(
    "Complete list of transactions in chronological order. Include every debit, credit, fee, and transfer. Do not omit any row from the transaction table."
  ),
  nsf_flag: z.boolean().describe(
    "True if any NSF (non-sufficient funds) or overdraft notices appear on the statement, or if any transaction balance goes negative."
  ),
});

export async function processBankStatement(filePath: string) {
  try {
    // Read local file and convert to data URL
    const fileBuffer = fs.readFileSync(filePath);
    const base64 = fileBuffer.toString("base64");
    const dataUrl = `data:application/pdf;base64,${base64}`;

    console.log(`Processing bank statement: ${filePath}`);

    // Step 1: Parse the PDF to markdown
    console.log("Step 1: Parsing PDF to markdown...");
    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}`);
    }

    // Combine all parsed chunks into one markdown string
    const markdown = parseRun.output.chunks
      .map((chunk) => chunk.content)
      .join("\n\n");

    console.log(`Parsed ${parseRun.output.chunks.length} chunks`);
    console.log(`Markdown preview (first 500 chars):\n${markdown.substring(0, 500)}...`);

    // Step 2: Extract structured fields using Zod schema
    console.log("\nStep 2: Extracting structured fields...");
    const extractRun = await client.extractRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        schema: bankStatementSchema,
        baseProcessor: "extraction_performance",
      },
    });

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

    const result = extractRun.output.value;

    // Validation and summary
    console.log("\n=== EXTRACTION RESULTS ===");
    console.log(`Bank: ${result.bank_name}`);
    console.log(`Account Holder: ${result.account_holder_name}`);
    console.log(`Account: ${result.account_number}`);
    console.log(
      `Period: ${result.statement_period_start_date} to ${result.statement_period_end_date}`
    );
    console.log(
      `Beginning Balance: $${result.beginning_balance.amount} ${result.beginning_balance.iso_4217_currency_code}`
    );
    console.log(
      `Ending Balance: $${result.ending_balance.amount} ${result.ending_balance.iso_4217_currency_code}`
    );
    console.log(
      `Total Deposits: $${result.total_deposits.amount} ${result.total_deposits.iso_4217_currency_code}`
    );
    console.log(
      `Total Withdrawals: $${result.total_withdrawals.amount} ${result.total_withdrawals.iso_4217_currency_code}`
    );
    console.log(
      `Total Fees: $${result.total_fees.amount} ${result.total_fees.iso_4217_currency_code}`
    );
    console.log(`Transaction Count: ${result.transactions.length}`);
    console.log(`NSF Flag: ${result.nsf_flag}`);

    // Sample transactions
    if (result.transactions.length > 0) {
      console.log("\nFirst 3 transactions:");
      result.transactions.slice(0, 3).forEach((tx, i) => {
        console.log(
          `  ${i + 1}. ${tx.transaction_date} | ${tx.transaction_type.toUpperCase()} | $${tx.amount} | ${tx.description?.substring(0, 50)}`
        );
      });
    }

    // Reconciliation check
    const calculatedBalance =
      (result.beginning_balance.amount ?? 0) +
      (result.total_deposits.amount ?? 0) -
      (result.total_withdrawals.amount ?? 0) -
      (result.total_fees.amount ?? 0);
    const expectedEndingBalance = result.ending_balance.amount ?? 0;
    const reconciled = Math.abs(calculatedBalance - expectedEndingBalance) < 0.01;

    console.log(
      `\nReconciliation: Calculated $${calculatedBalance.toFixed(2)} vs Reported $${expectedEndingBalance.toFixed(2)} — ${reconciled ? "✓ PASS" : "✗ FAIL"}`
    );

    return {
      success: true,
      data: result,
      reconciled,
      markdown,
    };
  } catch (error) {
    console.error("Error processing bank statement:", error);
    throw error;
  }
}

// Main entry point for testing
if (require.main === module) {
  const filePath = process.argv[2] || "./sample-bank-statement.pdf";
  processBankStatement(filePath)
    .then((result) => {
      console.log("\n✓ Processing complete");
      process.exit(0);
    })
    .catch((error) => {
      console.error("\n✗ Processing failed:", error.message);
      process.exit(1);
    });
}
```

## CLI equivalent

```bash
# Set API key
export EXTEND_API_KEY="sk_..."

# Parse the statement to markdown
extend parse bank_statement.pdf --output markdown.txt

# Extract fields using a pre-saved workflow or schema
extend extract bank_statement.pdf \
  --schema bank-statement-schema.json \
  --processor extraction_performance \
  --output extracted.json

# Or run a pre-built workflow (if saved)
extend run workflow_bank_processing --file bank_statement.pdf
```

## Schema

```typescript
// Full Zod schema for bank statement extraction
const bankStatementSchema = z.object({
  // Account metadata
  bank_name: z.string().nullable().describe(
    "Full legal name of the bank (e.g., 'JPMorgan Chase Bank, N.A.'). Look in the header or top-left of the statement."
  ),
  account_holder_name: z.string().nullable().describe(
    "Primary account holder name(s) as printed. Include all names if joint account, separated by 'AND' or 'OR'."
  ),
  account_number: z.string().nullable().describe(
    "Account number, usually masked as ****1234 or shown in full in the header. Extract exactly as displayed."
  ),

  // Statement period
  statement_period_start_date: extendDate().describe(
    "First day of the statement period in ISO 8601 (YYYY-MM-DD)."
  ),
  statement_period_end_date: extendDate().describe(
    "Last day of the statement period in ISO 8601 (YYYY-MM-DD)."
  ),

  // Balance summary
  beginning_balance: extendCurrency().describe(
    "Opening balance at start of period. Includes amount and ISO 4217 currency code (usually USD)."
  ),
  ending_balance: extendCurrency().describe(
    "Closing balance at end of period. Must reconcile as: beginning + deposits - withdrawals - fees = ending."
  ),
  total_deposits: extendCurrency().describe(
    "Sum of all inflows (paycheck deposits, transfers in, credits, refunds). Look for 'Deposits and Additions' line."
  ),
  total_withdrawals: extendCurrency().describe(
    "Sum of all outflows as a positive number (ATM withdrawals, debit purchases, checks, etc.). Look for 'Withdrawals' line."
  ),
  total_fees: extendCurrency().describe(
    "Total service fees charged in the period (monthly maintenance, overdraft, wire, NSF fees). Look for 'Fees' line."
  ),

  // Transactions
  transactions: z.array(z.object({
    transaction_date: extendDate().describe(
      "Transaction posting date in ISO 8601. Some statements show 'transaction date' and 'posting date' — use posting date."
    ),
    description: z.string().nullable().describe(
      "Complete merchant or counterparty name, location, reference number, and transaction ID. E.g., 'DISCOVER E-PAYMENT 6930 WEB ID: 2510020270' or 'ATM CASH DEPOSIT 12/29 13701 SW 88TH ST MIAMI FL'."
    ),
    amount: z.number().nullable().describe(
      "Signed transaction amount: positive for deposits/credits, negative for withdrawals/debits/fees. In USD or account currency."
    ),
    transaction_type: z.enum([
      "payment",      // Bill payment, check, credit card payment, Zelle
      "deposit",      // Paycheck, transfer in, cash deposit, refund
      "withdrawal",   // ATM cash, debit card purchase, recurring charge
      "transfer",     // Inter-account transfer, wire transfer
      "fee",          // Service charge, overdraft fee, wire fee
      "other",        // Unclear or unclassified
    ]).describe(
      "Transaction category. Use 'payment' for outgoing bill payments; 'withdrawal' for debit card purchases and cash; 'transfer' for inter-account or wire transfers."
    ),
    balance_after_transaction: z.number().nullable().describe(
      "Running balance immediately after this transaction. Used to detect NSF/overdraft conditions and validate extraction."
    ),
    nsf_related: z.boolean().describe(
      "True if marked NSF, or if balance after transaction goes negative (overdraft). False otherwise."
    ),
  })).describe(
    "All transactions in the statement, in chronological order. Do not skip or omit any row from the transaction table."
  ),

  // Flags
  nsf_flag: z.boolean().describe(
    "True if the statement contains an NSF (
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 bank statement schema using Zod
const transactionSchema = z.object({
  transaction_date: extendDate().describe(
    "Date of the transaction in ISO 8601 format (YYYY-MM-DD). Extract from the date column of the statement."
  ),
  description: z.string().nullable().describe(
    "Full transaction description, including merchant name, location, reference number, and transaction ID if present. Must capture the complete merchant or counterparty identifier."
  ),
  amount: z.number().nullable().describe(
    "Transaction amount as a signed number. Positive for deposits, negative for withdrawals/payments/fees."
  ),
  transaction_type: z.enum([
    "payment",
    "deposit",
    "withdrawal",
    "transfer",
    "fee",
    "other"
  ]).describe(
    "Categorize the transaction: 'payment' (bill payment, check, card payment), 'deposit' (paycheck, transfer in), 'withdrawal' (ATM, debit card purchase), 'transfer' (inter-account), 'fee' (service charge), 'other' if unclear."
  ),
  balance_after_transaction: z.number().nullable().describe(
    "Running balance after this transaction. Used to validate extraction accuracy and detect NSF conditions."
  ),
  nsf_related: z.boolean().describe(
    "True if this transaction is marked as NSF (non-sufficient funds) or if the balance after would be negative, indicating a potential overdraft condition."
  ),
});

const bankStatementSchema = z.object({
  bank_name: z.string().nullable().describe(
    "Full legal name of the bank (e.g., 'JPMorgan Chase Bank, N.A.'). Extract from the header or logo area."
  ),
  account_holder_name: z.string().nullable().describe(
    "Primary account holder name(s) as printed on the statement. Include all names separated by 'AND' or 'OR' as shown."
  ),
  account_number: z.string().nullable().describe(
    "Account number, usually masked as '****1234' or shown in full. Extract exactly as displayed."
  ),
  statement_period_start_date: extendDate().describe(
    "First day of the statement period in ISO 8601 format."
  ),
  statement_period_end_date: extendDate().describe(
    "Last day of the statement period in ISO 8601 format."
  ),
  beginning_balance: extendCurrency().describe(
    "Opening balance at the start of the statement period, including currency code."
  ),
  ending_balance: extendCurrency().describe(
    "Closing balance at the end of the statement period. Used to reconcile all transactions."
  ),
  total_deposits: extendCurrency().describe(
    "Sum of all credits/deposits during the period. Verify this matches sum of positive transactions."
  ),
  total_withdrawals: extendCurrency().describe(
    "Sum of all debits/withdrawals during the period (as a positive number). Verify this matches sum of negative transactions in absolute value."
  ),
  total_fees: extendCurrency().describe(
    "Total fees charged during the period. Extract from the 'Fees' line if present, or sum all fee transactions."
  ),
  transactions: z.array(transactionSchema).describe(
    "Complete list of transactions in chronological order. Include every debit, credit, fee, and transfer. Do not omit any row from the transaction table."
  ),
  nsf_flag: z.boolean().describe(
    "True if any NSF (non-sufficient funds) or overdraft notices appear on the statement, or if any transaction balance goes negative."
  ),
});

export async function processBankStatement(filePath: string) {
  try {
    // Read local file and convert to data URL
    const fileBuffer = fs.readFileSync(filePath);
    const base64 = fileBuffer.toString("base64");
    const dataUrl = `data:application/pdf;base64,${base64}`;

    console.log(`Processing bank statement: ${filePath}`);

    // Step 1: Parse the PDF to markdown
    console.log("Step 1: Parsing PDF to markdown...");
    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}`);
    }

    // Combine all parsed chunks into one markdown string
    const markdown = parseRun.output.chunks
      .map((chunk) => chunk.content)
      .join("\n\n");

    console.log(`Parsed ${parseRun.output.chunks.length} chunks`);
    console.log(`Markdown preview (first 500 chars):\n${markdown.substring(0, 500)}...`);

    // Step 2: Extract structured fields using Zod schema
    console.log("\nStep 2: Extracting structured fields...");
    const extractRun = await client.extractRuns.createAndPoll({
      file: { url: dataUrl },
      config: {
        schema: bankStatementSchema,
        baseProcessor: "extraction_performance",
      },
    });

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

    const result = extractRun.output.value;

    // Validation and summary
    console.log("\n=== EXTRACTION RESULTS ===");
    console.log(`Bank: ${result.bank_name}`);
    console.log(`Account Holder: ${result.account_holder_name}`);
    console.log(`Account: ${result.account_number}`);
    console.log(
      `Period: ${result.statement_period_start_date} to ${result.statement_period_end_date}`
    );
    console.log(
      `Beginning Balance: $${result.beginning_balance.amount} ${result.beginning_balance.iso_4217_currency_code}`
    );
    console.log(
      `Ending Balance: $${result.ending_balance.amount} ${result.ending_balance.iso_4217_currency_code}`
    );
    console.log(
      `Total Deposits: $${result.total_deposits.amount} ${result.total_deposits.iso_4217_currency_code}`
    );
    console.log(
      `Total Withdrawals: $${result.total_withdrawals.amount} ${result.total_withdrawals.iso_4217_currency_code}`
    );
    console.log(
      `Total Fees: $${result.total_fees.amount} ${result.total_fees.iso_4217_currency_code}`
    );
    console.log(`Transaction Count: ${result.transactions.length}`);
    console.log(`NSF Flag: ${result.nsf_flag}`);

    // Sample transactions
    if (result.transactions.length > 0) {
      console.log("\nFirst 3 transactions:");
      result.transactions.slice(0, 3).forEach((tx, i) => {
        console.log(
          `  ${i + 1}. ${tx.transaction_date} | ${tx.transaction_type.toUpperCase()} | $${tx.amount} | ${tx.description?.substring(0, 50)}`
        );
      });
    }

    // Reconciliation check
    const calculatedBalance =
      (result.beginning_balance.amount ?? 0) +
      (result.total_deposits.amount ?? 0) -
      (result.total_withdrawals.amount ?? 0) -
      (result.total_fees.amount ?? 0);
    const expectedEndingBalance = result.ending_balance.amount ?? 0;
    const reconciled = Math.abs(calculatedBalance - expectedEndingBalance) < 0.01;

    console.log(
      `\nReconciliation: Calculated $${calculatedBalance.toFixed(2)} vs Reported $${expectedEndingBalance.toFixed(2)} — ${reconciled ? "✓ PASS" : "✗ FAIL"}`
    );

    return {
      success: true,
      data: result,
      reconciled,
      markdown,
    };
  } catch (error) {
    console.error("Error processing bank statement:", error);
    throw error;
  }
}

// Main entry point for testing
if (require.main === module) {
  const filePath = process.argv[2] || "./sample-bank-statement.pdf";
  processBankStatement(filePath)
    .then((result) => {
      console.log("\n✓ Processing complete");
      process.exit(0);
    })
    .catch((error) => {
      console.error("\n✗ Processing failed:", error.message);
      process.exit(1);
    });
}
import os
from typing import Optional
from extend_ai import Extend
import base64

def process_bank_statement(file_path: str):
    """Process a bank statement PDF and extract structured financial data."""
    
    client = Extend(token=os.environ["EXTEND_API_KEY"])
    
    try:
        # Read local file and convert to data URL
        with open(file_path, "rb") as f:
            file_buffer = f.read()
        
        base64_str = base64.b64encode(file_buffer).decode("utf-8")
        data_url = f"data:application/pdf;base64,{base64_str}"
        
        print(f"Processing bank statement: {file_path}")
        
        # Step 1: Parse the PDF to markdown
        print("Step 1: Parsing PDF to markdown...")
        parse_run = client.parse_runs.create_and_poll(
            file={"url": data_url},
            config={
                "parse_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}")
        
        # Combine all parsed chunks into one markdown string
        markdown = "\n\n".join(
            chunk.content for chunk in parse_run.output.chunks
        )
        
        print(f"Parsed {len(parse_run.output.chunks)} chunks")
        print(f"Markdown preview (first 500 chars):\n{markdown[:500]}...")
        
        # Step 2: Extract structured fields using schema
        print("\nStep 2: Extracting structured fields...")
        
        bank_statement_schema = {
            "type": "object",
            "properties": {
                "bank_name": {
                    "type": ["string", "null"],
                    "description": "Full legal name of the bank (e.g., 'JPMorgan Chase Bank, N.A.'). Extract from the header or logo area.",
                },
                "account_holder_name": {
                    "type": ["string", "null"],
                    "description": "Primary account holder name(s) as printed on the statement. Include all names separated by 'AND' or 'OR' as shown.",
                },
                "account_number": {
                    "type": ["string", "null"],
                    "description": "Account number, usually masked as '****1234' or shown in full. Extract exactly as displayed.",
                },
                "statement_period_start_date": {
                    "type": ["string", "null"],
                    "description": "First day of the statement period in ISO 8601 format (YYYY-MM-DD).",
                },
                "statement_period_end_date": {
                    "type": ["string", "null"],
                    "description": "Last day of the statement period in ISO 8601 format (YYYY-MM-DD).",
                },
                "beginning_balance": {
                    "type": ["object", "null"],
                    "description": "Opening balance at the start of the statement period, including currency code.",
                    "properties": {
                        "amount": {"type": "number"},
                        "iso_4217_currency_code": {"type": "string"},
                    },
                },
                "ending_balance": {
                    "type": ["object", "null"],
                    "description": "Closing balance at the end of the statement period. Used to reconcile all transactions.",
                    "properties": {
                        "amount": {"type": "number"},
                        "iso_4217_currency_code": {"type": "string"},
                    },
                },
                "total_deposits": {
                    "type": ["object", "null"],
                    "description": "Sum of all credits/deposits during the period. Verify this matches sum of positive transactions.",
                    "properties": {
                        "amount": {"type": "number"},
                        "iso_4217_currency_code": {"type": "string"},
                    },
                },
                "total_withdrawals": {
                    "type": ["object", "null"],
                    "description": "Sum of all debits/withdrawals during the period (as a positive number). Verify this matches sum of negative transactions in absolute value.",
                    "properties": {
                        "amount": {"type": "number"},
                        "iso_4217_currency_code": {"type": "string"},
                    },
                },
                "total_fees": {
                    "type": ["object", "null"],
                    "description": "Total fees charged during the period. Extract from the 'Fees' line if present, or sum all fee transactions.",
                    "properties": {
                        "amount": {"type": "number"},
                        "iso_4217_currency_code": {"type": "string"},
                    },
                },
                "transactions": {
                    "type": "array",
                    "description": "Complete list of transactions in chronological order. Include every debit, credit, fee, and transfer. Do not omit any row from the transaction table.",
                    "items": {
                        "type": "object",
                        "properties": {
                            "transaction_date": {
                                "type": ["string", "null"],
                                "description": "Date of the transaction in ISO 8601 format (YYYY-MM-DD).",
                            },
                            "description": {
                                "type": ["string", "null"],
                                "description": "Full transaction description, including merchant name, location, reference number, and transaction ID if present.",
                            },
                            "amount": {
                                "type": ["number", "null"],
                                "description": "Transaction amount as a signed number. Positive for deposits, negative for withdrawals/payments/fees.",
                            },
                            "transaction_type": {
                                "type": ["string", "null"],
                                "enum": ["payment", "deposit", "withdrawal", "transfer", "fee", "other"],
                                "description": "Categorize the transaction.",
                            },
                            "balance_after_transaction": {
                                "type": ["number", "null"],
                                "description": "Running balance after this transaction.",
                            },
                            "nsf_related": {
                                "type": "boolean",
                                "description": "True if this transaction is marked as NSF or if the balance after would be negative.",
                            },
                        },
                    },
                },
                "nsf_flag": {
                    "type": "boolean",
                    "description": "True if any NSF (non-sufficient funds) or overdraft notices appear on the statement.",
                },
            },
        }
        
        extract_run = client.extract_runs.create_and_poll(
            file={"url": data_url},
            config={
                "schema": bank_statement_schema,
                "base_processor": "extraction_performance",
            },
        )
        
        if extract_run.status != "PROCESSED":
            raise Exception(f"Extraction failed with status: {extract_run.status}")
        
        result = extract_run.output.value
        
        # Validation and summary
        print("\n=== EXTRACTION RESULTS ===")
        print(f"Bank: {result.get('bank_name')}")
        print(f"Account Holder: {result.get('account_holder_name')}")
        print(f"Account: {result.get('account_number')}")
        print(
            f"Period: {result.get('statement_period_start_date')} to {result.get('statement_period_end_date')}"
        )
        
        bb = result.get("beginning_balance") or {}
        print(
            f"Beginning Balance: ${bb.get('amount')} {bb.get('iso_4217_currency_code', 'USD')}"
        )
        
        eb = result.get("ending_balance") or {}
        print(
            f"Ending Balance: ${eb.get('amount')} {eb.get('iso_4217_currency_code', 'USD')}"
        )
        
        td = result.get("total_deposits") or {}
        print(
            f"Total Deposits: ${td.get('amount')} {td.get('iso_4217_currency_code', 'USD')}"
        )
        
        tw = result.get("total_withdrawals") or {}
        print(
            f"Total Withdrawals: ${tw.get('amount')} {tw.get('iso_4217_currency_code', 'USD')}"
        )
        
        tf = result.get("total_fees") or {}
        print(
            f"Total Fees: ${tf.get('amount')} {tf.get('iso_4217_currency_code', 'USD')}"
        )
        
        transactions = result.get("transactions", [])
        print(f"Transaction Count: {len(transactions)}")
        print(f"NSF Flag: {result.get('nsf_flag')}")
        
        # Sample transactions
        if transactions:
            print("\nFirst 3 transactions:")
            for i, tx in enumerate(transactions[:3]):
                desc = tx.get("description", "")
                if desc:
                    desc = desc[:50]
                print(
                    f"  {i + 1}. {tx.get('transaction_date')} | {tx.get('transaction_type', '').upper()} | ${tx.get('amount')} | {desc}"
                )
        
        # Reconciliation check
        bb_amount = (bb.get("amount") or 0) if bb else 0
        td_amount = (td.get("amount") or 0) if td else 0
        tw_amount = (tw.get("amount") or 0) if tw else 0
        tf_amount = (tf.get("amount") or 0) if tf else 0
        eb_amount = (eb.get("amount") or 0) if eb else 0
        
        calculated_balance = bb_amount + td_amount - tw_amount - tf_amount
        reconciled = abs(calculated_balance - eb_amount) < 0.01
        
        print(
            f"\nReconciliation: Calculated ${calculated_balance:.2f} vs Reported ${eb_amount:.2f} — {'✓ PASS' if reconciled else '✗ FAIL'}"
        )
        
        return {
            "success": True,
            "data": result,
            "reconciled": reconciled,
            "markdown": markdown,
        }
        
    except Exception as error:
        print(f"Error processing bank statement: {error}")
        raise


if __name__ == "__main__":
    import sys
    
    file_path = sys.argv[1] if len(sys.argv) > 1 else "./sample-bank-statement.pdf"
    try:
        result = process_bank_statement(file_path)
        print("\n✓ Processing complete")
    except Exception as error:
        print(f"\n✗ Processing failed: {error}")
        sys.exit(1)
// This code uses Extend's REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints using Java's built-in HttpClient.

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;

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();

    static class CurrencyValue {
        public Double amount;
        public String iso_4217_currency_code;

        public CurrencyValue(Double amount, String code) {
            this.amount = amount;
            this.iso_4217_currency_code = code;
        }
    }

    static class Transaction {
        public String transaction_date;
        public String description;
        public Double amount;
        public String transaction_type;
        public Double balance_after_transaction;
        public Boolean nsf_related;
    }

    static class BankStatementResult {
        public String bank_name;
        public String account_holder_name;
        public String account_number;
        public String statement_period_start_date;
        public String statement_period_end_date;
        public CurrencyValue beginning_balance;
        public CurrencyValue ending_balance;
        public CurrencyValue total_deposits;
        public CurrencyValue total_withdrawals;
        public CurrencyValue total_fees;
        public List<Transaction> transactions;
        public Boolean nsf_flag;
    }

    static class ParseChunk {
        public String content;
    }

    static class ParseOutput {
        public List<ParseChunk> chunks;
    }

    static class ParseRunResponse {
        public String status;
        public ParseOutput output;
    }

    static class ExtractRunResponse {
        public String status;
        public Map<String, Object> output;
    }

    public static void main(String[] args) throws Exception {
        String filePath = args.length > 0 ? args[0] : "./sample-bank-statement.pdf";
        try {
            processBankStatement(filePath);
            System.out.println("\n✓ Processing complete");
            System.exit(0);
        } catch (Exception e) {
            System.err.println("\n✗ Processing failed: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }

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

            System.out.println("Processing bank statement: " + filePath);

            // Step 1: Parse the PDF to markdown
            System.out.println("Step 1: Parsing PDF to markdown...");
            ParseRunResponse parseRun = createAndPollParseRun(dataUrl);

            if (!"PROCESSED".equals(parseRun.status)) {
                throw new Exception("Parse failed with status: " + parseRun.status);
            }

            String markdown = parseRun.output.chunks.stream()
                    .map(chunk -> chunk.content)
                    .collect(Collectors.joining("\n\n"));

            System.out.println("Parsed " + parseRun.output.chunks.size() + " chunks");
            String preview = markdown.length() > 500 ? markdown.substring(0, 500) + "..." : markdown;
            System.out.println("Markdown preview (first 500 chars):\n" + preview);

            // Step 2: Extract structured fields
            System.out.println("\nStep 2: Extracting structured fields...");
            ExtractRunResponse extractRun = createAndPollExtractRun(dataUrl);

            if (!"PROCESSED".equals(extractRun.status)) {
                throw new Exception("Extraction failed with status: " + extractRun.status);
            }

            BankStatementResult result = parseBankStatementResult(extractRun.output);

            // Validation and summary
            System.out.println("\n=== EXTRACTION RESULTS ===");
            System.out.println("Bank: " + result.bank_name);
            System.out.println("Account Holder: " + result.account_holder_name);
            System.out.println("Account: " + result.account_number);
            System.out.println("Period: " + result.statement_period_start_date + " to " + result.statement_period_end_date);
            System.out.println("Beginning Balance: $" + result.beginning_balance.amount + " " + result.beginning_balance.iso_4217_currency_code);
            System.out.println("Ending Balance: $" + result.ending_balance.amount + " " + result.ending_balance.iso_4217_currency_code);
            System.out.println("Total Deposits: $" + result.total_deposits.amount + " " + result.total_deposits.iso_4217_currency_code);
            System.out.println("Total Withdrawals: $" + result.total_withdrawals.amount + " " + result.total_withdrawals.iso_4217_currency_code);
            System.out.println("Total Fees: $" + result.total_fees.amount + " " + result.total_fees.iso_4217_currency_code);
            System.out.println("Transaction Count: " + result.transactions.size());
            System.out.println("NSF Flag: " + result.nsf_flag);

            // Sample transactions
            if (!result.transactions.isEmpty()) {
                System.out.println("\nFirst 3 transactions:");
                for (int i = 0; i < Math.min(3, result.transactions.size()); i++) {
                    Transaction tx = result.transactions.get(i);
                    String desc = tx.description != null && tx.description.length() > 50
                            ? tx.description.substring(0, 50)
                            : tx.description;
                    System.out.println(String.format("  %d. %s | %s | $%s | %s",
                            i + 1, tx.transaction_date, tx.transaction_type.toUpperCase(),
                            tx.amount, desc));
                }
            }

            // Reconciliation check
            double calculatedBalance = (result.beginning_balance.amount != null ? result.beginning_balance.amount : 0)
                    + (result.total_deposits.amount != null ? result.total_deposits.amount : 0)
                    - (result.total_withdrawals.amount != null ? result.total_withdrawals.amount : 0)
                    - (result.total_fees.amount != null ? result.total_fees.amount : 0);
            double expectedEndingBalance = result.ending_balance.amount != null ? result.ending_balance.amount : 0;
            boolean reconciled = Math.abs(calculatedBalance - expectedEndingBalance) < 0.01;

            System.out.println(String.format("\nReconciliation: Calculated $%.2f vs Reported $%.2f — %s",
                    calculatedBalance, expectedEndingBalance, reconciled ? "✓ PASS" : "✗ FAIL"));

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

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

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE_URL + "/parseRuns/createAndPoll"))
                .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());
        return parseJson(response.body(), ParseRunResponse.class);
    }

    private static ExtractRunResponse createAndPollExtractRun(String dataUrl) throws Exception {
        String schemaJson = "{\"type\":\"object\",\"properties\":{\"bank_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the financial institution\"},\"account_holder_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Primary account holder name\"},\"account_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Account number\"},\"statement_period_start_date\":{\"type\":[\"string\",\"null\"],\"description\":\"First day of statement period\"},\"statement_period_end_date\":{\"type\":[\"string\",\"null\"],\"description\":\"Last day of statement period\"},\"beginning_balance\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":\"number\"},\"iso_4217_currency_code\":{\"type\":\"string\"}}},\"ending_balance\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":\"number\"},\"iso_4217_currency_code\":{\"type\":\"string\"}}},\"total_deposits\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":\"number\"},\"iso_4217_currency_code\":{\"type\":\"string\"}}},\"total_withdrawals\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":\"number\"},\"iso_4217_currency_code\":{\"type\":\"string\"}}},\"total_fees\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":\"number\"},\"iso_4217_currency_code\":{\"type\":\"string\"}}},\"transactions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"transaction_date\":{\"type\":\"string\"},\"description\":{\"type\":[\"string\",\"null\"]},\"amount\":{\"type\":[\"number\",\"null\"]},\"transaction_type\":{\"type\":\"string\",\"enum\":[\"payment\",\"deposit\",\"withdrawal\",\"transfer\",\"fee\",\"other\"]},\"balance_after_transaction\":{\"type\":[\"number\",\"null\"]},\"nsf_related\":{\"type\":\"boolean\"}}}},\"nsf_flag\":{\"type\":\"boolean\"}}}";

        String requestBody = String.format(
                "{\"file\":{\"url\":\"%s\"},\"config\":{\"schema\":%s,\"baseProcessor\":\"extraction_performance\"}}",
                dataUrl.replace("\"", "\\\""), schemaJson);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE_URL + "/extractRuns/createAndPoll"))
                .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());
        return parseJson(response.body(), ExtractRunResponse.class);
    }

    @SuppressWarnings("unchecked")
    private static BankStatementResult parseBankStatementResult(Map<String, Object> output) {
        BankStatementResult result = new BankStatementResult();
        result.bank_name = (String) output.get("bank_name");
        result.account_holder_name = (String) output.get("account_holder_name");
        result.account_number = (String) output.get("account_number");
        result.statement_period_start_date = (String) output.get("statement_period_start_date");
        result.statement_period_end_date = (String) output.get("statement_period_end_date");
        result.nsf_flag = (Boolean) output.getOrDefault("nsf_flag", false);

        result.beginning_balance = parseCurrency((Map<String, Object>) output.get("beginning_balance"));
        result.ending_balance = parseCurrency((Map<String, Object>) output.get("ending_balance"));
        result.total_deposits = parseCurrency((Map<String, Object>) output.get("total_deposits"));
        result.total_withdrawals = parseCurrency((Map<String, Object>) output.get("total_withdrawals"));
        result.total_fees = parseCurrency((Map<String, Object>) output.get("total_fees"));

        result.transactions = new ArrayList<>();
        List<Map<String, Object>> txList = (List<Map<String, Object>>) output.get("transactions");
        if (txList != null) {
            for (Map<String, Object> txMap : txList) {
                Transaction tx = new Transaction();
                tx.transaction_date = (String) txMap.get("transaction_date");
                tx.description = (String) txMap.get("description");
                Object amountObj = txMap.get("amount");
                tx.amount = amountObj instanceof Number ? ((Number) amountObj).doubleValue() : null;
                tx.transaction_type = (String) txMap.get("transaction_type");
                Object balanceObj = txMap.get("balance_after_transaction");
                tx.balance_after_transaction = balanceObj instanceof Number ? ((Number) balanceObj).doubleValue() : null;
                tx.nsf_related = (Boolean) txMap.getOrDefault("nsf_related", false);
                result.transactions.add(tx);
            }
        }

        return result;
    }

    private static CurrencyValue parseCurrency(Map<String, Object> currencyMap) {
        if (currencyMap == null) {
            return new CurrencyValue(null, "USD");
        }
        Object amountObj = currencyMap.get("amount");
        Double amount = amountObj instanceof Number ? ((Number) amountObj).doubleValue() : null;
        String code = (String) currencyMap.getOrDefault("iso_4217_currency_code", "USD");
        return new CurrencyValue(amount, code);
    }

    @SuppressWarnings("unchecked")
    private static <T> T parseJson(String json, Class<T> type) {
        // Simple JSON parsing for this use case
        if (type == ParseRunResponse.class) {
            ParseRunResponse response = new ParseRunResponse();
            response.status = extractJsonString(json, "status");
            response.output = new ParseOutput();
            response.output.chunks = new ArrayList<>();

            int chunksStart = json.indexOf("\"chunks\"");
            if (chunksStart != -1) {
                int arrayStart = json.indexOf("[", chunksStart);
                int arrayEnd = json.lastIndexOf("]");
                String chunksJson = json.substring(arrayStart, arrayEnd + 1);
                String[] chunkObjs = chunksJson.split("\\{");
                for (String chunkObj : chunkObjs) {
                    if (chunkObj.contains("content")) {
                        String content = extractJsonString("{" + chunkObj, "content");
                        if (content != null) {
                            ParseChunk chunk = new ParseChunk();
                            chunk.content = content;
                            response.output.chunks.add(chunk);
                        }
                    }
                }
            }
            return (T) response;
        } else if (type == ExtractRunResponse.class) {
            ExtractRunResponse response = new ExtractRunResponse();
            response.status = extractJsonString(json, "status");
            response.output = parseJsonObject(json, "value");
            return (T) response;
        }
        return null;
    }

    private static String extractJsonString(String json, String key) {
        String searchKey = "\"" + key + "\"";
        int keyIndex = json.indexOf(searchKey);
        if (keyIndex == -1) return null;

        int colonIndex = json.indexOf(":", keyIndex);
        int quoteStart = json.indexOf("\"", colonIndex);
        if (quoteStart == -1) return null;

        int quoteEnd = json.indexOf("\"", quoteStart + 1);
        while (quoteEnd > 0 && json.charAt(quoteEnd - 1) == '\\') {
            quoteEnd = json.indexOf("\"", quoteEnd + 1);
        }
        return quoteEnd > quoteStart ? json.substring(quoteStart + 1, quoteEnd) : null;
    }

    @SuppressWarnings("unchecked")
    private static Map<String, Object> parseJsonObject(String json, String key) {
        String searchKey = "\"" + key + "\"";
        int keyIndex = json.indexOf(searchKey);
        if (keyIndex == -1) return new HashMap<>();

        int objectStart = json.indexOf("{", keyIndex);
        int depth = 1;
        int i = objectStart + 1;
        while (i < json.length() && depth > 0) {
            if (json.charAt(i) == '{') depth++;
            else if (json.charAt(i) == '}') depth--;
            i++;
        }

        String objectJson = json.substring(objectStart, i);
        Map<String, Object> result = new HashMap<>();

        String[] pairs = objectJson.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
        for (String pair : pairs) {
            int colonIdx = pair.indexOf(":");
            if (colonIdx > 0) {
                String pairKey = pair.substring(0, colonIdx).replaceAll("[{\"\\s]", "");
                String pairValue = pair.substring(colonIdx + 1).trim();
                if (pairValue.startsWith("\"")) {
                    result.put(pairKey, pairValue.replaceAll("[\"\\}]", ""));
                } else if (pairValue.startsWith("[")) {
                    result.put(pairKey, parseJsonArray(pairValue));
                } else if (pairValue.startsWith("{")) {
                    result.put(pairKey, parseJsonObject(pair, pairKey));
                } else {
                    try {
                        result.put(pairKey, Double.parseDouble(pairValue));
                    } catch (NumberFormatException e) {
                        result.put(pairKey, pairValue);
                    }
                }
            }
        }

        return result;
    }

    @SuppressWarnings("unchecked")
    private static List<Object> parseJsonArray(String json) {
        List<Object> result = new ArrayList<>();
        int start = json.indexOf("[");
        int end = json.lastIndexOf("]");
        if (start == -1 || end == -1) return result;

        String content = json.substring(start + 1, end);
        if (content.isEmpty()) return result;

        int depth = 0;
        StringBuilder current = new StringBuilder();
        for (int i = 0; i < content.length(); i++) {
            char c = content.charAt(i);
            if ((c == '{' || c == '[')) depth++;
            else if ((c == '}' || c == ']')) depth--;
            else if (c == ',' && depth == 0) {
                result.add(current.toString().trim());
                current = new StringBuilder();
                continue;
            }
            current.append(c);
        }
        if (current.length() > 0) {
            result.add(current.toString().trim());
        }

        return result;
    }
}
// This example uses the Extend REST API directly because Extend has no official Go SDK yet.
// All HTTP calls use only Go's standard net/http and encoding/json packages.

package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"net/http"
	"os"
	"time"
)

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

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

type Transaction struct {
	TransactionDate       string  `json:"transaction_date"`
	Description           *string `json:"description"`
	Amount                *float64 `json:"amount"`
	TransactionType       string  `json:"transaction_type"`
	BalanceAfterTx        *float64 `json:"balance_after_transaction"`
	NSFRelated            bool    `json:"nsf_related"`
}

type BankStatementResult struct {
	BankName                 *string       `json:"bank_name"`
	AccountHolderName        *string       `json:"account_holder_name"`
	AccountNumber            *string       `json:"account_number"`
	StatementPeriodStartDate string        `json:"statement_period_start_date"`
	StatementPeriodEndDate   string        `json:"statement_period_end_date"`
	BeginningBalance         CurrencyValue `json:"beginning_balance"`
	EndingBalance            CurrencyValue `json:"ending_balance"`
	TotalDeposits            CurrencyValue `json:"total_deposits"`
	TotalWithdrawals         CurrencyValue `json:"total_withdrawals"`
	TotalFees                CurrencyValue `json:"total_fees"`
	Transactions             []Transaction `json:"transactions"`
	NSFFlag                  bool          `json:"nsf_flag"`
}

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

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

type ParseRun struct {
	Status string      `json:"status"`
	Output ParseOutput `json:"output"`
}

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

type ExtractRun struct {
	Status string        `json:"status"`
	Output ExtractOutput `json:"output"`
}

type ProcessResult struct {
	Success    bool
	Data       BankStatementResult
	Reconciled bool
	Markdown   string
}

func makeRequest(method, path string, body interface{}) ([]byte, error) {
	var reqBody *bytes.Buffer
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewBuffer(data)
	} else {
		reqBody = bytes.NewBuffer([]byte{})
	}

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

	token := os.Getenv("EXTEND_API_KEY")
	if token == "" {
		return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
	}

	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 := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("API error: %d %s", resp.StatusCode, string(respBody))
	}

	return respBody, nil
}

func pollParseRun(runID string) (*ParseRun, error) {
	for i := 0; i < 120; i++ {
		respBody, err := makeRequest("GET", "/runs/parse/"+runID, nil)
		if err != nil {
			return nil, err
		}

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

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

		time.Sleep(1 * time.Second)
	}

	return nil, fmt.Errorf("parse run polling timeout")
}

func pollExtractRun(runID string) (*ExtractRun, error) {
	for i := 0; i < 120; i++ {
		respBody, err := makeRequest("GET", "/runs/extract/"+runID, nil)
		if err != nil {
			return nil, err
		}

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

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

		time.Sleep(1 * time.Second)
	}

	return nil, fmt.Errorf("extract run polling timeout")
}

func processBankStatement(filePath string) (*ProcessResult, error) {
	// Read file and convert to data URL
	fileBuffer, err := ioutil.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

	base64Data := base64.StdEncoding.EncodeToString(fileBuffer)
	dataURL := "data:application/pdf;base64," + base64Data

	log.Printf("Processing bank statement: %s", filePath)

	// Step 1: Parse the PDF to markdown
	log.Println("Step 1: Parsing PDF to markdown...")

	parseReqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"parseConfig": map[string]interface{}{
				"blockOptions": map[string]interface{}{
					"text": map[string]interface{}{
						"agentic": map[string]bool{
							"enabled": true,
						},
					},
				},
				"chunkingStrategy": map[string]string{
					"type": "document",
				},
			},
		},
	}

	parseResp, err := makeRequest("POST", "/runs/parse", parseReqBody)
	if err != nil {
		return nil, err
	}

	var parseRunResp map[string]interface{}
	if err := json.Unmarshal(parseResp, &parseRunResp); err != nil {
		return nil, err
	}

	runID := parseRunResp["id"].(string)
	parseRun, err := pollParseRun(runID)
	if err != nil {
		return nil, err
	}

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

	// Combine all parsed chunks into markdown
	var markdown string
	for _, chunk := range parseRun.Output.Chunks {
		markdown += chunk.Content + "\n\n"
	}

	log.Printf("Parsed %d chunks", len(parseRun.Output.Chunks))
	if len(markdown) > 500 {
		log.Printf("Markdown preview (first 500 chars):\n%s...", markdown[:500])
	} else {
		log.Printf("Markdown preview:\n%s", markdown)
	}

	// Step 2: Extract structured fields
	log.Println("\nStep 2: Extracting structured fields...")

	bankStatementSchema := 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",
			},
			"account_holder_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Primary account holder name(s)",
			},
			"account_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Account number",
			},
			"statement_period_start_date": map[string]interface{}{
				"type":        "string",
				"description": "First day of statement period (YYYY-MM-DD)",
			},
			"statement_period_end_date": map[string]interface{}{
				"type":        "string",
				"description": "Last day of statement period (YYYY-MM-DD)",
			},
			"beginning_balance": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]string{
						"type": "string",
					},
				},
			},
			"ending_balance": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]string{
						"type": "string",
					},
				},
			},
			"total_deposits": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]string{
						"type": "string",
					},
				},
			},
			"total_withdrawals": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]string{
						"type": "string",
					},
				},
			},
			"total_fees": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]string{
						"type": "string",
					},
				},
			},
			"transactions": map[string]interface{}{
				"type": "array",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"transaction_date": map[string]string{
							"type": "string",
						},
						"description": map[string]interface{}{
							"type": []string{"string", "null"},
						},
						"amount": map[string]interface{}{
							"type": []string{"number", "null"},
						},
						"transaction_type": map[string]interface{}{
							"type": "string",
							"enum": []string{"payment", "deposit", "withdrawal", "transfer", "fee", "other"},
						},
						"balance_after_transaction": map[string]interface{}{
							"type": []string{"number", "null"},
						},
						"nsf_related": map[string]string{
							"type": "boolean",
						},
					},
				},
			},
			"nsf_flag": map[string]string{
				"type": "boolean",
			},
		},
	}

	extractReqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"schema":         bankStatementSchema,
			"baseProcessor":  "extraction_performance",
		},
	}

	extractResp, err := makeRequest("POST", "/runs/extract", extractReqBody)
	if err != nil {
		return nil, err
	}

	var extractRunResp map[string]interface{}
	if err := json.Unmarshal(extractResp, &extractRunResp); err != nil {
		return nil, err
	}

	extractRunID := extractRunResp["id"].(string)
	extractRun, err := pollExtractRun(extractRunID)
	if err != nil {
		return nil, err
	}

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

	result := extractRun.Output.Value

	// Validation and summary
	log.Println("\n=== EXTRACTION RESULTS ===")
	if result.BankName != nil {
		log.Printf("Bank: %s", *result.BankName)
	}
	if result.AccountHolderName != nil {
		log.Printf("Account Holder: %s", *result.AccountHolderName)
	}
	if result.AccountNumber != nil {
		log.Printf("Account: %s", *result.AccountNumber)
	}
	log.Printf("Period: %s to %s", result.StatementPeriodStartDate, result.StatementPeriodEndDate)

	if result.BeginningBalance.Amount != nil {
		log.Printf("Beginning Balance: $%v %s", *result.BeginningBalance.Amount, result.BeginningBalance.ISO4217CurrencyCode)
	}
	if result.EndingBalance.Amount != nil {
		log.Printf("Ending Balance: $%v %s", *result.EndingBalance.Amount, result.EndingBalance.ISO4217CurrencyCode)
	}
	if result.TotalDeposits.Amount != nil {
		log.Printf("Total Deposits: $%v %s", *result.TotalDeposits.Amount, result.TotalDeposits.ISO4217CurrencyCode)
	}
	if result.TotalWithdrawals.Amount != nil {
		log.Printf("Total Withdrawals: $%v %s", *result.TotalWithdrawals.Amount, result.TotalWithdrawals.ISO4217CurrencyCode)
	}
	if result.TotalFees.Amount != nil {
		log.Printf("Total Fees: $%v %s", *result.TotalFees.Amount, result.TotalFees.ISO4217CurrencyCode)
	}
	log.Printf("Transaction Count: %d", len(result.Transactions))
	log.Printf("NSF Flag: %v", result.NSFFlag)

	// Sample transactions
	if len(result.Transactions) > 0 {
		log.Println("\nFirst 3 transactions:")
		max := 3
		if len(result.Transactions) < 3 {
			max = len(result.Transactions)
		}
		for i := 0; i < max; i++ {
			tx := result.Transactions[i]
			desc := ""
			if tx.Description != nil {
				desc = *tx.Description
				if len(desc) > 50 {
					desc = desc[:50]
				}
			}
			amount := ""
			if tx.Amount != nil {
				amount = fmt.Sprintf("$%v", *tx.Amount)
			}
			log.Printf("  %d. %s | %s | %s | %s", i+1, tx.TransactionDate, tx.TransactionType, amount, desc)
		}
	}

	// Reconciliation check
	beginningBal := 0.0
	if result.BeginningBalance.Amount != nil {
		beginningBal = *result.BeginningBalance.Amount
	}
	totalDeps := 0.0
	if result.TotalDeposits.Amount != nil {
		totalDeps = *result.TotalDeposits.Amount
	}
	totalWiths := 0.0
	if result.TotalWithdrawals.Amount != nil {
		totalWiths = *result.TotalWithdrawals.Amount
	}
	totalFeesAmt := 0.0
	if result.TotalFees.Amount != nil {
		totalFeesAmt = *result.TotalFees.Amount
	}

	calculatedBalance := beginningBal + totalDeps - totalWiths - totalFeesAmt
	expectedEndingBalance := 0.0
	if result.EndingBalance.Amount != nil {
		expectedEndingBalance = *result.EndingBalance.Amount
	}

	reconciled := math.Abs(calculatedBalance-expectedEndingBalance) < 0.01
	status := "✓ PASS"
	if !reconciled {
		status = "✗ FAIL"
	}

	log.Printf("\nReconciliation: Calculated $%.2f vs Reported $%.2f — %s", calculatedBalance, expectedEndingBalance, status)

	return &ProcessResult{
		Success:    true,
		Data:       result,
		Reconciled: reconciled,
		Markdown:   markdown,
	}, nil
}

func main() {
	filePath := "./sample-bank-statement.pdf"
	if len(os.Args) > 1 {
		filePath = os.Args[1]
	}

	result, err := processBankStatement(filePath)
	if err != nil {
		log.Printf("\n✗ Processing failed: %v", err)
		os.Exit(1)
	}

	log.Println("\n✓ Processing complete")
	os.Exit(0)
}
// 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.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).

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.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": {
              "statement_period_start": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Start date of the statement period in YYYY-MM-DD format"
              },
              "statement_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "End date of the statement period in YYYY-MM-DD format"
              },
              "account_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Bank account number"
              },
              "account_holder_names": {
                "type": "array",
                "description": "List of account holder names"
              },
              "account_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Type of account (e.g., High School Checking)"
              },
              "beginning_balance": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Opening balance amount in dollars"
              },
              "ending_balance": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Closing balance amount in dollars"
              },
              "total_deposits": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total deposits and additions in dollars"
              },
              "total_withdrawals": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total withdrawals (ATM, debit, and electronic) in dollars"
              },
              "total_fees": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total fees charged in dollars"
              },
              "bank_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the financial institution"
              },
              "transactions": {
                "type": "array",
                "description": "List of individual transactions"
              }
            }
          }
        }
      }
    }
  ]
};

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 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.json"


def load_state() -> dict:
    if STATE_FILE.exists():
        with open(STATE_FILE, "r") as f:
            return json.load(f)
    return {}


def save_state(state: dict) -> None:
    STATE_DIR.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, 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": {
                            "statement_period_start": {
                                "type": ["string", "null"],
                                "description": "Start date of the statement period in YYYY-MM-DD format",
                            },
                            "statement_period_end": {
                                "type": ["string", "null"],
                                "description": "End date of the statement period in YYYY-MM-DD format",
                            },
                            "account_number": {
                                "type": ["string", "null"],
                                "description": "Bank account number",
                            },
                            "account_holder_names": {
                                "type": "array",
                                "description": "List of account holder names",
                            },
                            "account_type": {
                                "type": ["string", "null"],
                                "description": "Type of account (e.g., High School Checking)",
                            },
                            "beginning_balance": {
                                "type": ["number", "null"],
                                "description": "Opening balance amount in dollars",
                            },
                            "ending_balance": {
                                "type": ["number", "null"],
                                "description": "Closing balance amount in dollars",
                            },
                            "total_deposits": {
                                "type": ["number", "null"],
                                "description": "Total deposits and additions in dollars",
                            },
                            "total_withdrawals": {
                                "type": ["number", "null"],
                                "description": "Total withdrawals (ATM, debit, and electronic) in dollars",
                            },
                            "total_fees": {
                                "type": ["number", "null"],
                                "description": "Total fees charged in dollars",
                            },
                            "bank_name": {
                                "type": ["string", "null"],
                                "description": "Name of the financial institution",
                            },
                            "transactions": {
                                "type": "array",
                                "description": "List of individual transactions",
                            },
                        },
                    }
                }
            },
        },
    ],
}


async def main() -> None:
    client = Extend(token=API_KEY)
    state = load_state()

    print(f'Deploying "{WORKFLOW["name"]}…"')

    if state.get("workflow_id"):
        print(
            f'✓ workflow already provisioned ({state["workflow_id"]}) — updating steps'
        )
        await client.workflows.update(
            id=state["workflow_id"], steps=WORKFLOW["steps"]
        )
    else:
        existing_id = None
        try:
            workflows_response = await client.workflows.list(
                name=WORKFLOW["name"]
            )
            items = workflows_response.data if hasattr(workflows_response, "data") else []
            for item in items:
                if item.name == WORKFLOW["name"]:
                    existing_id = item.id
                    break
        except Exception:
            pass

        if existing_id:
            state["workflow_id"] = existing_id
            save_state(state)
            print(
                f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps'
            )
            await client.workflows.update(id=existing_id, steps=WORKFLOW["steps"])
        else:
            created = await client.workflows.create(**WORKFLOW)
            wf_id = created.id
            if not wf_id:
                raise RuntimeError("Could not read created workflow id from response")
            state["workflow_id"] = wf_id
            save_state(state)
            print(f"+ created workflow ({wf_id})")

    try:
        await client.workflows.create_version(id=state["workflow_id"])
    except Exception:
        pass

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


if __name__ == "__main__":
    import asyncio

    try:
        asyncio.run(main())
    except Exception as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)
// This script uses the Extend REST API directly because Extend has no official Java SDK yet.
// 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.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)
//   javac Provision.java && java Provision
//
// Generated by doc1 (template: bank-statement).

import java.io.*;
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.*;
import java.util.*;

public class Provision {
  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.json");

  static {
    if (API_KEY == null || API_KEY.isEmpty()) {
      System.err.println("Set EXTEND_API_KEY first.");
      System.exit(1);
    }
  }

  static class State {
    String workflowId;
  }

  private static State state = loadState();

  private static State loadState() {
    if (Files.exists(STATE_FILE)) {
      try {
        String json = Files.readString(STATE_FILE);
        return parseStateJson(json);
      } catch (IOException e) {
        return new State();
      }
    }
    return new State();
  }

  private static State parseStateJson(String json) {
    State s = new State();
    if (json.contains("\"workflowId\"")) {
      int start = json.indexOf("\"workflowId\"") + 14;
      int end = json.indexOf("\"", start);
      s.workflowId = json.substring(start, end);
    }
    return s;
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    StringBuilder sb = new StringBuilder();
    sb.append("{\n");
    if (state.workflowId != null) {
      sb.append("  \"workflowId\": \"").append(escape(state.workflowId)).append("\"\n");
    }
    sb.append("}\n");
    Files.writeString(STATE_FILE, sb.toString());
  }

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

  private static String apiCall(String method, String pathName, String body) throws Exception {
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest.Builder rb = HttpRequest.newBuilder()
        .uri(URI.create(API + pathName))
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

    if (body != null) {
      rb.header("Content-Type", "application/json");
      rb.method(method, HttpRequest.BodyPublishers.ofString(body));
    } else {
      rb.method(method, HttpRequest.BodyPublishers.noBody());
    }

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

    String data = response.body();
    if (response.statusCode() < 200 || response.statusCode() >= 300) {
      String preview = data.length() > 300 ? data.substring(0, 300) : data;
      throw new RuntimeException(method + " " + pathName + " failed (" + response.statusCode() + "): " + preview);
    }
    return data;
  }

  private static String buildWorkflowJson() {
    return "{\n" +
        "  \"name\": \"Bank Statement Processing Pipeline\",\n" +
        "  \"steps\": [\n" +
        "    {\n" +
        "      \"name\": \"startTrigger1\",\n" +
        "      \"type\": \"TRIGGER\",\n" +
        "      \"next\": [\n" +
        "        {\n" +
        "          \"step\": \"parse1\"\n" +
        "        }\n" +
        "      ]\n" +
        "    },\n" +
        "    {\n" +
        "      \"name\": \"parse1\",\n" +
        "      \"type\": \"PARSE\",\n" +
        "      \"config\": {\n" +
        "        \"parseConfig\": {\n" +
        "          \"blockOptions\": {\n" +
        "            \"text\": {\n" +
        "              \"agentic\": {\n" +
        "                \"enabled\": true\n" +
        "              }\n" +
        "            }\n" +
        "          },\n" +
        "          \"chunkingStrategy\": {\n" +
        "            \"type\": \"document\"\n" +
        "          }\n" +
        "        }\n" +
        "      },\n" +
        "      \"next\": [\n" +
        "        {\n" +
        "          \"step\": \"extraction2\"\n" +
        "        }\n" +
        "      ]\n" +
        "    },\n" +
        "    {\n" +
        "      \"name\": \"extraction2\",\n" +
        "      \"type\": \"EXTRACT\",\n" +
        "      \"config\": {\n" +
        "        \"extractorConfig\": {\n" +
        "          \"schema\": {\n" +
        "            \"type\": \"object\",\n" +
        "            \"properties\": {\n" +
        "              \"statement_period_start\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Start date of the statement period in YYYY-MM-DD format\"\n" +
        "              },\n" +
        "              \"statement_period_end\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"End date of the statement period in YYYY-MM-DD format\"\n" +
        "              },\n" +
        "              \"account_number\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Bank account number\"\n" +
        "              },\n" +
        "              \"account_holder_names\": {\n" +
        "                \"type\": \"array\",\n" +
        "                \"description\": \"List of account holder names\"\n" +
        "              },\n" +
        "              \"account_type\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Type of account (e.g., High School Checking)\"\n" +
        "              },\n" +
        "              \"beginning_balance\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Opening balance amount in dollars\"\n" +
        "              },\n" +
        "              \"ending_balance\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Closing balance amount in dollars\"\n" +
        "              },\n" +
        "              \"total_deposits\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Total deposits and additions in dollars\"\n" +
        "              },\n" +
        "              \"total_withdrawals\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Total withdrawals (ATM, debit, and electronic) in dollars\"\n" +
        "              },\n" +
        "              \"total_fees\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Total fees charged in dollars\"\n" +
        "              },\n" +
        "              \"bank_name\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Name of the financial institution\"\n" +
        "              },\n" +
        "              \"transactions\": {\n" +
        "                \"type\": \"array\",\n" +
        "                \"description\": \"List of individual transactions\"\n" +
        "              }\n" +
        "            }\n" +
        "          }\n" +
        "        }\n" +
        "      }\n" +
        "    }\n" +
        "  ]\n" +
        "}\n";
  }

  private static String buildStepsJson() {
    return "{\n" +
        "  \"steps\": [\n" +
        "    {\n" +
        "      \"name\": \"startTrigger1\",\n" +
        "      \"type\": \"TRIGGER\",\n" +
        "      \"next\": [\n" +
        "        {\n" +
        "          \"step\": \"parse1\"\n" +
        "        }\n" +
        "      ]\n" +
        "    },\n" +
        "    {\n" +
        "      \"name\": \"parse1\",\n" +
        "      \"type\": \"PARSE\",\n" +
        "      \"config\": {\n" +
        "        \"parseConfig\": {\n" +
        "          \"blockOptions\": {\n" +
        "            \"text\": {\n" +
        "              \"agentic\": {\n" +
        "                \"enabled\": true\n" +
        "              }\n" +
        "            }\n" +
        "          },\n" +
        "          \"chunkingStrategy\": {\n" +
        "            \"type\": \"document\"\n" +
        "          }\n" +
        "        }\n" +
        "      },\n" +
        "      \"next\": [\n" +
        "        {\n" +
        "          \"step\": \"extraction2\"\n" +
        "        }\n" +
        "      ]\n" +
        "    },\n" +
        "    {\n" +
        "      \"name\": \"extraction2\",\n" +
        "      \"type\": \"EXTRACT\",\n" +
        "      \"config\": {\n" +
        "        \"extractorConfig\": {\n" +
        "          \"schema\": {\n" +
        "            \"type\": \"object\",\n" +
        "            \"properties\": {\n" +
        "              \"statement_period_start\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Start date of the statement period in YYYY-MM-DD format\"\n" +
        "              },\n" +
        "              \"statement_period_end\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"End date of the statement period in YYYY-MM-DD format\"\n" +
        "              },\n" +
        "              \"account_number\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Bank account number\"\n" +
        "              },\n" +
        "              \"account_holder_names\": {\n" +
        "                \"type\": \"array\",\n" +
        "                \"description\": \"List of account holder names\"\n" +
        "              },\n" +
        "              \"account_type\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Type of account (e.g., High School Checking)\"\n" +
        "              },\n" +
        "              \"beginning_balance\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Opening balance amount in dollars\"\n" +
        "              },\n" +
        "              \"ending_balance\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Closing balance amount in dollars\"\n" +
        "              },\n" +
        "              \"total_deposits\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Total deposits and additions in dollars\"\n" +
        "              },\n" +
        "              \"total_withdrawals\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Total withdrawals (ATM, debit, and electronic) in dollars\"\n" +
        "              },\n" +
        "              \"total_fees\": {\n" +
        "                \"type\": [\"number\", \"null\"],\n" +
        "                \"description\": \"Total fees charged in dollars\"\n" +
        "              },\n" +
        "              \"bank_name\": {\n" +
        "                \"type\": [\"string\", \"null\"],\n" +
        "                \"description\": \"Name of the financial institution\"\n" +
        "              },\n" +
        "              \"transactions\": {\n" +
        "                \"type\": \"array\",\n" +
        "                \"description\": \"List of individual transactions\"\n" +
        "              }\n" +
        "            }\n" +
        "          }\n" +
        "        }\n" +
        "      }\n" +
        "    }\n" +
        "  ]\n" +
        "}\n";
  }

  private static String extractId(String json) {
    int idIdx = json.indexOf("\"id\"");
    if (idIdx < 0) {
      idIdx = json.indexOf("\"workflow\"");
      if (idIdx < 0) return null;
      idIdx = json.indexOf("\"id\"", idIdx);
      if (idIdx < 0) return null;
    }
    int start = json.indexOf("\"", idIdx) + 1;
    int end = json.indexOf("\"", start);
    return json.substring(start, end);
  }

  private static String extractWorkflowName(String json) {
    int idx = json.indexOf("\"name\"");
    if (idx < 0) return null;
    int start = json.indexOf("\"", idx + 6) + 1;
    int end = json.indexOf("\"", start);
    return json.substring(start, end);
  }

  public static void main(String[] args) {
    try {
      System.out.println("Deploying \"Bank Statement Processing Pipeline\"…");

      if (state.workflowId != null) {
        System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
        apiCall("POST", "/workflows/" + state.workflowId, buildStepsJson());
      } else {
        boolean found = false;
        try {
          String encoded = URLEncoder.encode("Bank Statement Processing Pipeline", StandardCharsets.UTF_8);
          String listResponse = apiCall("GET", "/workflows?name=" + encoded, null);
          if (listResponse.contains("\"id\"")) {
            String[] items = listResponse.split("\\{");
            for (String item : items) {
              if (item.contains("\"name\"")) {
                String name = extractWorkflowName("{" + item);
                if ("Bank Statement Processing Pipeline".equals(name)) {
                  String id = extractId("{" + item);
                  if (id != null) {
                    state.workflowId = id;
                    saveState();
                    System.out.println("✓ workflow \"Bank Statement Processing Pipeline\" found in your account (" + id + ") — updating steps");
                    apiCall("POST", "/workflows/" + id, buildStepsJson());
                    found = true;
                    break;
                  }
                }
              }
            }
          }
        } catch (Exception e) {
          // lookup is best-effort; fall through to create
        }

        if (!found) {
          String createResponse = apiCall("POST", "/workflows", buildWorkflowJson());
          String wfId = extractId(createResponse);
          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 {
        apiCall("POST", "/workflows/" + state.workflowId + "/versions", "{}");
      } 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.");
    } catch (Exception e) {
      System.err.println(e.getMessage() != null ? e.getMessage() : e);
      System.exit(1);
    }
  }
}
// This code calls the Extend REST API directly (base URL https://api.extend.ai).
// Extend does not publish an official Go SDK, so we use only the standard
// net/http and encoding/json packages with zero external dependencies.

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"
)

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

type WorkflowDefinition struct {
	Name  string        `json:"name"`
	Steps []interface{} `json:"steps"`
}

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

	stateDir := filepath.Join(".", ".extend")
	stateFile := filepath.Join(stateDir, "bank-statement.json")

	state := &State{}
	if data, err := os.ReadFile(stateFile); err == nil {
		_ = json.Unmarshal(data, state)
	}

	saveState := func() error {
		if err := os.MkdirAll(stateDir, 0755); err != nil {
			return err
		}
		data, _ := json.MarshalIndent(state, "", "  ")
		return os.WriteFile(stateFile, data, 0644)
	}

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

		req, _ := http.NewRequest(method, API+pathName, reqBody)
		req.Header.Set("Authorization", "Bearer "+apiKey)
		req.Header.Set("x-extend-api-version", VERSION)
		if body != nil {
			req.Header.Set("Content-Type", "application/json")
		}

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

		respData := make(map[string]interface{})
		_ = json.NewDecoder(resp.Body).Decode(&respData)

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

		return respData, nil
	}

	workflow := WorkflowDefinition{
		Name: "Bank Statement Processing Pipeline",
		Steps: []interface{}{
			map[string]interface{}{
				"name": "startTrigger1",
				"type": "TRIGGER",
				"next": []map[string]interface{}{
					{"step": "parse1"},
				},
			},
			map[string]interface{}{
				"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"},
				},
			},
			map[string]interface{}{
				"name": "extraction2",
				"type": "EXTRACT",
				"config": map[string]interface{}{
					"extractorConfig": map[string]interface{}{
						"schema": map[string]interface{}{
							"type": "object",
							"properties": map[string]interface{}{
								"statement_period_start": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Start date of the statement period in YYYY-MM-DD format",
								},
								"statement_period_end": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "End date of the statement period in YYYY-MM-DD format",
								},
								"account_number": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Bank account number",
								},
								"account_holder_names": map[string]interface{}{
									"type":        "array",
									"description": "List of account holder names",
								},
								"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{"number", "null"},
									"description": "Opening balance amount in dollars",
								},
								"ending_balance": map[string]interface{}{
									"type":        []string{"number", "null"},
									"description": "Closing balance amount in dollars",
								},
								"total_deposits": map[string]interface{}{
									"type":        []string{"number", "null"},
									"description": "Total deposits and additions in dollars",
								},
								"total_withdrawals": map[string]interface{}{
									"type":        []string{"number", "null"},
									"description": "Total withdrawals (ATM, debit, and electronic) in dollars",
								},
								"total_fees": map[string]interface{}{
									"type":        []string{"number", "null"},
									"description": "Total fees charged in dollars",
								},
								"bank_name": map[string]interface{}{
									"type":        []string{"string", "null"},
									"description": "Name of the financial institution",
								},
								"transactions": map[string]interface{}{
									"type":        "array",
									"description": "List of individual transactions",
								},
							},
						},
					},
				},
			},
		},
	}

	fmt.Printf("Deploying \"%s\"…\n", workflow.Name)

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		_, _ = apiCall("POST", "/workflows/"+state.WorkflowID, map[string]interface{}{"steps": workflow.Steps})
	} else {
		// Try to find an existing workflow with the same name.
		query := url.QueryEscape(workflow.Name)
		if list, err := apiCall("GET", "/workflows?name="+query, nil); err == nil {
			var items []map[string]interface{}
			if data, ok := list["data"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			} else if data, ok := list["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 == workflow.Name {
					if id, ok := item["id"].(string); ok {
						state.WorkflowID = id
						_ = saveState()
						fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, id)
						_, _ = apiCall("POST", "/workflows/"+id, map[string]interface{}{"steps": workflow.Steps})
						break
					}
				}
			}
		}

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

			wfID := ""
			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", "/workflows/"+state.WorkflowID+"/versions", 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)

Use `mode: "agentic_ocr"` in parse config—it's designed for structured layouts and will preserve table relationships in the markdown output, making extraction more accurate.
Define your schema's `transactions` array with flexible fields (`type: ["string", "null"]`) and write descriptions that match common bank terminology (date, amount, balance, memo).
Confidence for numeric fields is typically 0.85–0.95 on clean statements; always validate extracted account numbers and totals against checksums or OCR confidence flags before storing.
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
Tags
Bank StatementAccount SummaryTransaction DetailFinancial Records
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
    Vendor Invoice ExtractorParse → Extract
    Extracts charges, billing details, line items, totals, and due dates vendor invoices.
    PDFImages & Scanswww.extend.ai/templates/vendor-invoice
  4. 04
    Pay Stub ExtractorParse → Extract
    Extracts employee earnings, deductions, taxes, and net pay from pay stubs.
    PDFwww.extend.ai/templates/pay-stub
  5. 05
    Check ExtractorParse → Extract
    Extracts check details including payee, amount, date, and bank routing information.
    PDFImages & Scanswww.extend.ai/templates/check
  6. 06
    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
  7. 07
    Onboarding Package ExtractorParse → Extract
    Extracts account summaries, transaction details, and balances from bank statements.
    PDFImages & Scanswww.extend.ai/templates/personal-bank-statement
  8. 08
    1040 Tax Return ExtractorParse → Extract
    Extracts personal and dependent information from U.S. individual income tax returns.
    PDFImages & Scanswww.extend.ai/templates/tax-return-form-1040