Extracts account summaries, balances, deposits, NSF flags, and transaction details from bank statements.
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.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledtruechangedchunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
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…changedmetadata.account_holder_name.logprobsConfidencenullchangedmetadata.account_holder_name.ocrConfidence0.992changedmetadata.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,…changedmetadata.account_number.logprobsConfidencenullchangedmetadata.account_number.ocrConfidence0.992changedmetadata.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}…changedmetadata.bank_name.logprobsConfidencenullchangedmetadata.bank_name.ocrConfidence0.993changedmetadata.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…changedmetadata.beginning_balance.logprobsConfidencenullchangedmetadata.beginning_balance.ocrConfidence0.995changedmetadata.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,…changedmetadata.ending_balance.logprobsConfidencenullchangedmetadata.ending_balance.ocrConfidence0.995changedmetadata.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,…changedmetadata.nsf_flag.logprobsConfidencenullchangedmetadata.nsf_flag.ocrConfidence0.996changedmetadata.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"…changedmetadata.statement_period_end_date.logprobsConfidencenullchangedmetadata.statement_period_end_date.ocrConfidence0.989changedmetadata.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"…changedmetadata.statement_period_start_date.logprobsConfidencenullchangedmetadata.statement_period_start_date.ocrConfidence0.99changedmetadata.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…changedmetadata.total_deposits.logprobsConfidencenullchangedmetadata.total_deposits.ocrConfidence0.992changedmetadata.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":…changedmetadata.total_fees.logprobsConfidencenullchangedmetadata.total_fees.ocrConfidence0.995changedmetadata.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…changedmetadata.total_withdrawals.logprobsConfidencenullchangedmetadata.total_withdrawals.ocrConfidence0.989changedmetadata.transactions.citations[]changedmetadata.transactions.logprobsConfidencenullchangedmetadata.transactions.ocrConfidence0.655changedmetadata.transactions[0].amount.logprobsConfidencenullchangedmetadata.transactions[0].amount.ocrConfidence0.996changedmetadata.transactions[0].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[0].balance_after_transaction.ocrConfidence0.998changedmetadata.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}]…changedmetadata.transactions[0].description.logprobsConfidencenullchangedmetadata.transactions[0].description.ocrConfidence0.985changedmetadata.transactions[0].logprobsConfidencenullchangedmetadata.transactions[0].nsf_related.logprobsConfidencenullchangedmetadata.transactions[0].ocrConfidence0.985changedmetadata.transactions[0].transaction_date.logprobsConfidencenullchangedmetadata.transactions[0].transaction_date.ocrConfidence0.99changedmetadata.transactions[0].transaction_type.logprobsConfidencenullchangedmetadata.transactions[0].transaction_type.ocrConfidence0.994changedmetadata.transactions[10].amount.logprobsConfidencenullchangedmetadata.transactions[10].amount.ocrConfidence0.995changedmetadata.transactions[10].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[10].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[10].description.logprobsConfidencenullchangedmetadata.transactions[10].description.ocrConfidence0.988changedmetadata.transactions[10].logprobsConfidencenullchangedmetadata.transactions[10].nsf_related.logprobsConfidencenullchangedmetadata.transactions[10].ocrConfidence0.988changedmetadata.transactions[10].transaction_date.logprobsConfidencenullchangedmetadata.transactions[10].transaction_date.ocrConfidence0.989changedmetadata.transactions[10].transaction_type.logprobsConfidencenullchangedmetadata.transactions[10].transaction_type.ocrConfidence0.993changedmetadata.transactions[11].amount.logprobsConfidencenullchangedmetadata.transactions[11].amount.ocrConfidence0.995changedmetadata.transactions[11].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[11].balance_after_transaction.ocrConfidence0.995changedmetadata.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":…changedmetadata.transactions[11].description.logprobsConfidencenullchangedmetadata.transactions[11].description.ocrConfidence0.985changedmetadata.transactions[11].logprobsConfidencenullchangedmetadata.transactions[11].nsf_related.logprobsConfidencenullchangedmetadata.transactions[11].ocrConfidence0.985changedmetadata.transactions[11].transaction_date.logprobsConfidencenullchangedmetadata.transactions[11].transaction_date.ocrConfidence0.991changedmetadata.transactions[11].transaction_type.logprobsConfidencenullchangedmetadata.transactions[11].transaction_type.ocrConfidence0.993changedmetadata.transactions[12].amount.logprobsConfidencenullchangedmetadata.transactions[12].amount.ocrConfidence0.995changedmetadata.transactions[12].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[12].balance_after_transaction.ocrConfidence0.995changedmetadata.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…changedmetadata.transactions[12].description.logprobsConfidencenullchangedmetadata.transactions[12].description.ocrConfidence0.9changedmetadata.transactions[12].logprobsConfidencenullchangedmetadata.transactions[12].nsf_related.logprobsConfidencenullchangedmetadata.transactions[12].ocrConfidence0.9changedmetadata.transactions[12].transaction_date.logprobsConfidencenullchangedmetadata.transactions[12].transaction_date.ocrConfidence0.991changedmetadata.transactions[12].transaction_type.logprobsConfidencenullchangedmetadata.transactions[12].transaction_type.ocrConfidence0.993changedmetadata.transactions[13].amount.logprobsConfidencenullchangedmetadata.transactions[13].amount.ocrConfidence0.997changedmetadata.transactions[13].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[13].balance_after_transaction.ocrConfidence0.998changedmetadata.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…changedmetadata.transactions[13].description.logprobsConfidencenullchangedmetadata.transactions[13].description.ocrConfidence0.916changedmetadata.transactions[13].logprobsConfidencenullchangedmetadata.transactions[13].nsf_related.logprobsConfidencenullchangedmetadata.transactions[13].ocrConfidence0.916changedmetadata.transactions[13].transaction_date.logprobsConfidencenullchangedmetadata.transactions[13].transaction_date.ocrConfidence0.991changedmetadata.transactions[13].transaction_type.logprobsConfidencenullchangedmetadata.transactions[13].transaction_type.ocrConfidence0.992changedmetadata.transactions[14].amount.logprobsConfidencenullchangedmetadata.transactions[14].amount.ocrConfidence0.992changedmetadata.transactions[14].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[14].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[14].description.logprobsConfidencenullchangedmetadata.transactions[14].description.ocrConfidence0.964changedmetadata.transactions[14].logprobsConfidencenullchangedmetadata.transactions[14].nsf_related.logprobsConfidencenullchangedmetadata.transactions[14].ocrConfidence0.964changedmetadata.transactions[14].transaction_date.logprobsConfidencenullchangedmetadata.transactions[14].transaction_date.ocrConfidence0.991changedmetadata.transactions[14].transaction_type.logprobsConfidencenullchangedmetadata.transactions[14].transaction_type.ocrConfidence0.992changedmetadata.transactions[15].amount.logprobsConfidencenullchangedmetadata.transactions[15].amount.ocrConfidence0.992changedmetadata.transactions[15].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[15].balance_after_transaction.ocrConfidence0.995changedmetadata.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…changedmetadata.transactions[15].description.logprobsConfidencenullchangedmetadata.transactions[15].description.ocrConfidence0.881changedmetadata.transactions[15].logprobsConfidencenullchangedmetadata.transactions[15].nsf_related.logprobsConfidencenullchangedmetadata.transactions[15].ocrConfidence0.881changedmetadata.transactions[15].transaction_date.logprobsConfidencenullchangedmetadata.transactions[15].transaction_date.ocrConfidence0.991changedmetadata.transactions[15].transaction_type.logprobsConfidencenullchangedmetadata.transactions[15].transaction_type.ocrConfidence0.994changedmetadata.transactions[16].amount.logprobsConfidencenullchangedmetadata.transactions[16].amount.ocrConfidence0.655changedmetadata.transactions[16].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[16].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[16].description.logprobsConfidencenullchangedmetadata.transactions[16].description.ocrConfidence0.985changedmetadata.transactions[16].logprobsConfidencenullchangedmetadata.transactions[16].nsf_related.logprobsConfidencenullchangedmetadata.transactions[16].ocrConfidence0.655changedmetadata.transactions[16].transaction_date.logprobsConfidencenullchangedmetadata.transactions[16].transaction_date.ocrConfidence0.991changedmetadata.transactions[16].transaction_type.logprobsConfidencenullchangedmetadata.transactions[16].transaction_type.ocrConfidence0.994changedmetadata.transactions[17].amount.logprobsConfidencenullchangedmetadata.transactions[17].amount.ocrConfidence0.992changedmetadata.transactions[17].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[17].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[17].description.logprobsConfidencenullchangedmetadata.transactions[17].description.ocrConfidence0.964changedmetadata.transactions[17].logprobsConfidencenullchangedmetadata.transactions[17].nsf_related.logprobsConfidencenullchangedmetadata.transactions[17].ocrConfidence0.964changedmetadata.transactions[17].transaction_date.logprobsConfidencenullchangedmetadata.transactions[17].transaction_date.ocrConfidence0.991changedmetadata.transactions[17].transaction_type.logprobsConfidencenullchangedmetadata.transactions[17].transaction_type.ocrConfidence0.993changedmetadata.transactions[18].amount.logprobsConfidencenullchangedmetadata.transactions[18].amount.ocrConfidence0.995changedmetadata.transactions[18].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[18].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[18].description.logprobsConfidencenullchangedmetadata.transactions[18].description.ocrConfidence0.985changedmetadata.transactions[18].logprobsConfidencenullchangedmetadata.transactions[18].nsf_related.logprobsConfidencenullchangedmetadata.transactions[18].ocrConfidence0.985changedmetadata.transactions[18].transaction_date.logprobsConfidencenullchangedmetadata.transactions[18].transaction_date.ocrConfidence0.991changedmetadata.transactions[18].transaction_type.logprobsConfidencenullchangedmetadata.transactions[18].transaction_type.ocrConfidence0.993changedmetadata.transactions[19].amount.logprobsConfidencenullchangedmetadata.transactions[19].amount.ocrConfidence0.655changedmetadata.transactions[19].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[19].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[19].description.logprobsConfidencenullchangedmetadata.transactions[19].description.ocrConfidence0.975changedmetadata.transactions[19].logprobsConfidencenullchangedmetadata.transactions[19].nsf_related.logprobsConfidencenullchangedmetadata.transactions[19].ocrConfidence0.655changedmetadata.transactions[19].transaction_date.logprobsConfidencenullchangedmetadata.transactions[19].transaction_date.ocrConfidence0.991changedmetadata.transactions[19].transaction_type.logprobsConfidencenullchangedmetadata.transactions[19].transaction_type.ocrConfidence0.994changedmetadata.transactions[1].amount.logprobsConfidencenullchangedmetadata.transactions[1].amount.ocrConfidence0.996changedmetadata.transactions[1].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[1].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[1].description.logprobsConfidencenullchangedmetadata.transactions[1].description.ocrConfidence0.989changedmetadata.transactions[1].logprobsConfidencenullchangedmetadata.transactions[1].nsf_related.logprobsConfidencenullchangedmetadata.transactions[1].ocrConfidence0.989changedmetadata.transactions[1].transaction_date.logprobsConfidencenullchangedmetadata.transactions[1].transaction_date.ocrConfidence0.99changedmetadata.transactions[1].transaction_type.logprobsConfidencenullchangedmetadata.transactions[1].transaction_type.ocrConfidence0.994changedmetadata.transactions[20].amount.logprobsConfidencenullchangedmetadata.transactions[20].amount.ocrConfidence0.983changedmetadata.transactions[20].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[20].balance_after_transaction.ocrConfidence0.995changedmetadata.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…changedmetadata.transactions[20].description.logprobsConfidencenullchangedmetadata.transactions[20].description.ocrConfidence0.956changedmetadata.transactions[20].logprobsConfidencenullchangedmetadata.transactions[20].nsf_related.logprobsConfidencenullchangedmetadata.transactions[20].ocrConfidence0.956changedmetadata.transactions[20].transaction_date.logprobsConfidencenullchangedmetadata.transactions[20].transaction_date.ocrConfidence0.991changedmetadata.transactions[20].transaction_type.logprobsConfidencenullchangedmetadata.transactions[20].transaction_type.ocrConfidence0.992changedmetadata.transactions[21].amount.logprobsConfidencenullchangedmetadata.transactions[21].amount.ocrConfidence0.983changedmetadata.transactions[21].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[21].balance_after_transaction.ocrConfidence0.995changedmetadata.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}],…changedmetadata.transactions[21].description.logprobsConfidencenullchangedmetadata.transactions[21].description.ocrConfidence0.964changedmetadata.transactions[21].logprobsConfidencenullchangedmetadata.transactions[21].nsf_related.logprobsConfidencenullchangedmetadata.transactions[21].ocrConfidence0.964changedmetadata.transactions[21].transaction_date.logprobsConfidencenullchangedmetadata.transactions[21].transaction_date.ocrConfidence0.991changedmetadata.transactions[21].transaction_type.logprobsConfidencenullchangedmetadata.transactions[21].transaction_type.ocrConfidence0.992changedmetadata.transactions[22].amount.logprobsConfidencenullchangedmetadata.transactions[22].amount.ocrConfidence0.987changedmetadata.transactions[22].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[22].balance_after_transaction.ocrConfidence0.989changedmetadata.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…changedmetadata.transactions[22].description.logprobsConfidencenullchangedmetadata.transactions[22].description.ocrConfidence0.916changedmetadata.transactions[22].logprobsConfidencenullchangedmetadata.transactions[22].nsf_related.logprobsConfidencenullchangedmetadata.transactions[22].ocrConfidence0.916changedmetadata.transactions[22].transaction_date.logprobsConfidencenullchangedmetadata.transactions[22].transaction_date.ocrConfidence0.991changedmetadata.transactions[22].transaction_type.logprobsConfidencenullchangedmetadata.transactions[22].transaction_type.ocrConfidence0.992changedmetadata.transactions[23].amount.logprobsConfidencenullchangedmetadata.transactions[23].amount.ocrConfidence0.995changedmetadata.transactions[23].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[23].balance_after_transaction.ocrConfidence0.994changedmetadata.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…changedmetadata.transactions[23].description.logprobsConfidencenullchangedmetadata.transactions[23].description.ocrConfidence0.916changedmetadata.transactions[23].logprobsConfidencenullchangedmetadata.transactions[23].nsf_related.logprobsConfidencenullchangedmetadata.transactions[23].ocrConfidence0.916changedmetadata.transactions[23].transaction_date.logprobsConfidencenullchangedmetadata.transactions[23].transaction_date.ocrConfidence0.991changedmetadata.transactions[23].transaction_type.logprobsConfidencenullchangedmetadata.transactions[23].transaction_type.ocrConfidence0.993changedmetadata.transactions[24].amount.logprobsConfidencenullchangedmetadata.transactions[24].amount.ocrConfidence0.996changedmetadata.transactions[24].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[24].balance_after_transaction.ocrConfidence0.992changedmetadata.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…changedmetadata.transactions[24].description.logprobsConfidencenullchangedmetadata.transactions[24].description.ocrConfidence0.916changedmetadata.transactions[24].logprobsConfidencenullchangedmetadata.transactions[24].nsf_related.logprobsConfidencenullchangedmetadata.transactions[24].ocrConfidence0.916changedmetadata.transactions[24].transaction_date.logprobsConfidencenullchangedmetadata.transactions[24].transaction_date.ocrConfidence0.991changedmetadata.transactions[24].transaction_type.logprobsConfidencenullchangedmetadata.transactions[24].transaction_type.ocrConfidence0.993changedmetadata.transactions[25].amount.logprobsConfidencenullchangedmetadata.transactions[25].amount.ocrConfidence0.992changedmetadata.transactions[25].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[25].balance_after_transaction.ocrConfidence0.967changedmetadata.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}…changedmetadata.transactions[25].description.logprobsConfidencenullchangedmetadata.transactions[25].description.ocrConfidence0.959changedmetadata.transactions[25].logprobsConfidencenullchangedmetadata.transactions[25].nsf_related.logprobsConfidencenullchangedmetadata.transactions[25].ocrConfidence0.959changedmetadata.transactions[25].transaction_date.logprobsConfidencenullchangedmetadata.transactions[25].transaction_date.ocrConfidence0.991changedmetadata.transactions[25].transaction_type.logprobsConfidencenullchangedmetadata.transactions[25].transaction_type.ocrConfidence0.992changedmetadata.transactions[26].amount.logprobsConfidencenullchangedmetadata.transactions[26].amount.ocrConfidence0.995changedmetadata.transactions[26].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[26].balance_after_transaction.ocrConfidence0.995changedmetadata.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}],…changedmetadata.transactions[26].description.logprobsConfidencenullchangedmetadata.transactions[26].description.ocrConfidence0.984changedmetadata.transactions[26].logprobsConfidencenullchangedmetadata.transactions[26].nsf_related.logprobsConfidencenullchangedmetadata.transactions[26].ocrConfidence0.984changedmetadata.transactions[26].transaction_date.logprobsConfidencenullchangedmetadata.transactions[26].transaction_date.ocrConfidence0.991changedmetadata.transactions[26].transaction_type.logprobsConfidencenullchangedmetadata.transactions[26].transaction_type.ocrConfidence0.993changedmetadata.transactions[27].amount.logprobsConfidencenullchangedmetadata.transactions[27].amount.ocrConfidence0.994changedmetadata.transactions[27].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[27].balance_after_transaction.ocrConfidence0.995changedmetadata.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"…changedmetadata.transactions[27].description.logprobsConfidencenullchangedmetadata.transactions[27].description.ocrConfidence0.881changedmetadata.transactions[27].logprobsConfidencenullchangedmetadata.transactions[27].nsf_related.logprobsConfidencenullchangedmetadata.transactions[27].ocrConfidence0.881changedmetadata.transactions[27].transaction_date.logprobsConfidencenullchangedmetadata.transactions[27].transaction_date.ocrConfidence0.991changedmetadata.transactions[27].transaction_type.logprobsConfidencenullchangedmetadata.transactions[27].transaction_type.ocrConfidence0.994changedmetadata.transactions[28].amount.logprobsConfidencenullchangedmetadata.transactions[28].amount.ocrConfidence0.983changedmetadata.transactions[28].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[28].balance_after_transaction.ocrConfidence0.995changedmetadata.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}…changedmetadata.transactions[28].description.logprobsConfidencenullchangedmetadata.transactions[28].description.ocrConfidence0.881changedmetadata.transactions[28].logprobsConfidencenullchangedmetadata.transactions[28].nsf_related.logprobsConfidencenullchangedmetadata.transactions[28].ocrConfidence0.881changedmetadata.transactions[28].transaction_date.logprobsConfidencenullchangedmetadata.transactions[28].transaction_date.ocrConfidence0.991changedmetadata.transactions[28].transaction_type.logprobsConfidencenullchangedmetadata.transactions[28].transaction_type.ocrConfidence0.994changedmetadata.transactions[29].amount.logprobsConfidencenullchangedmetadata.transactions[29].amount.ocrConfidence0.985changedmetadata.transactions[29].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[29].balance_after_transaction.ocrConfidence0.992changedmetadata.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…changedmetadata.transactions[29].description.logprobsConfidencenullchangedmetadata.transactions[29].description.ocrConfidence0.83changedmetadata.transactions[29].logprobsConfidencenullchangedmetadata.transactions[29].nsf_related.logprobsConfidencenullchangedmetadata.transactions[29].ocrConfidence0.83changedmetadata.transactions[29].transaction_date.logprobsConfidencenullchangedmetadata.transactions[29].transaction_date.ocrConfidence0.991changedmetadata.transactions[29].transaction_type.logprobsConfidencenullchangedmetadata.transactions[29].transaction_type.ocrConfidence0.992changedmetadata.transactions[2].amount.logprobsConfidencenullchangedmetadata.transactions[2].amount.ocrConfidence0.995changedmetadata.transactions[2].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[2].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[2].description.logprobsConfidencenullchangedmetadata.transactions[2].description.ocrConfidence0.977changedmetadata.transactions[2].logprobsConfidencenullchangedmetadata.transactions[2].nsf_related.logprobsConfidencenullchangedmetadata.transactions[2].ocrConfidence0.977changedmetadata.transactions[2].transaction_date.logprobsConfidencenullchangedmetadata.transactions[2].transaction_date.ocrConfidence0.99changedmetadata.transactions[2].transaction_type.logprobsConfidencenullchangedmetadata.transactions[2].transaction_type.ocrConfidence0.993changedmetadata.transactions[30].amount.logprobsConfidencenullchangedmetadata.transactions[30].amount.ocrConfidence0.994changedmetadata.transactions[30].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[30].balance_after_transaction.ocrConfidence0.998changedmetadata.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…changedmetadata.transactions[30].description.logprobsConfidencenullchangedmetadata.transactions[30].description.ocrConfidence0.964changedmetadata.transactions[30].logprobsConfidencenullchangedmetadata.transactions[30].nsf_related.logprobsConfidencenullchangedmetadata.transactions[30].ocrConfidence0.964changedmetadata.transactions[30].transaction_date.logprobsConfidencenullchangedmetadata.transactions[30].transaction_date.ocrConfidence0.991changedmetadata.transactions[30].transaction_type.logprobsConfidencenullchangedmetadata.transactions[30].transaction_type.ocrConfidence0.994changedmetadata.transactions[31].amount.logprobsConfidencenullchangedmetadata.transactions[31].amount.ocrConfidence0.993changedmetadata.transactions[31].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[31].balance_after_transaction.ocrConfidence0.997changedmetadata.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,"…changedmetadata.transactions[31].description.logprobsConfidencenullchangedmetadata.transactions[31].description.ocrConfidence0.982changedmetadata.transactions[31].logprobsConfidencenullchangedmetadata.transactions[31].nsf_related.logprobsConfidencenullchangedmetadata.transactions[31].ocrConfidence0.982changedmetadata.transactions[31].transaction_date.logprobsConfidencenullchangedmetadata.transactions[31].transaction_date.ocrConfidence0.991changedmetadata.transactions[31].transaction_type.logprobsConfidencenullchangedmetadata.transactions[31].transaction_type.ocrConfidence0.998changedmetadata.transactions[32].amount.logprobsConfidencenullchangedmetadata.transactions[32].amount.ocrConfidence0.993changedmetadata.transactions[32].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[32].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[32].description.logprobsConfidencenullchangedmetadata.transactions[32].description.ocrConfidence0.881changedmetadata.transactions[32].logprobsConfidencenullchangedmetadata.transactions[32].nsf_related.logprobsConfidencenullchangedmetadata.transactions[32].ocrConfidence0.881changedmetadata.transactions[32].transaction_date.logprobsConfidencenullchangedmetadata.transactions[32].transaction_date.ocrConfidence0.991changedmetadata.transactions[32].transaction_type.logprobsConfidencenullchangedmetadata.transactions[32].transaction_type.ocrConfidence0.994changedmetadata.transactions[33].amount.logprobsConfidencenullchangedmetadata.transactions[33].amount.ocrConfidence0.992changedmetadata.transactions[33].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[33].balance_after_transaction.ocrConfidence0.998changedmetadata.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…changedmetadata.transactions[33].description.logprobsConfidencenullchangedmetadata.transactions[33].description.ocrConfidence0.977changedmetadata.transactions[33].logprobsConfidencenullchangedmetadata.transactions[33].nsf_related.logprobsConfidencenullchangedmetadata.transactions[33].ocrConfidence0.977changedmetadata.transactions[33].transaction_date.logprobsConfidencenullchangedmetadata.transactions[33].transaction_date.ocrConfidence0.991changedmetadata.transactions[33].transaction_type.logprobsConfidencenullchangedmetadata.transactions[33].transaction_type.ocrConfidence0.993changedmetadata.transactions[34].amount.logprobsConfidencenullchangedmetadata.transactions[34].amount.ocrConfidence0.996changedmetadata.transactions[34].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[34].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[34].description.logprobsConfidencenullchangedmetadata.transactions[34].description.ocrConfidence0.985changedmetadata.transactions[34].logprobsConfidencenullchangedmetadata.transactions[34].nsf_related.logprobsConfidencenullchangedmetadata.transactions[34].ocrConfidence0.985changedmetadata.transactions[34].transaction_date.logprobsConfidencenullchangedmetadata.transactions[34].transaction_date.ocrConfidence0.991changedmetadata.transactions[34].transaction_type.logprobsConfidencenullchangedmetadata.transactions[34].transaction_type.ocrConfidence0.993changedmetadata.transactions[35].amount.logprobsConfidencenullchangedmetadata.transactions[35].amount.ocrConfidence0.996changedmetadata.transactions[35].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[35].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[35].description.logprobsConfidencenullchangedmetadata.transactions[35].description.ocrConfidence0.985changedmetadata.transactions[35].logprobsConfidencenullchangedmetadata.transactions[35].nsf_related.logprobsConfidencenullchangedmetadata.transactions[35].ocrConfidence0.985changedmetadata.transactions[35].transaction_date.logprobsConfidencenullchangedmetadata.transactions[35].transaction_date.ocrConfidence0.991changedmetadata.transactions[35].transaction_type.logprobsConfidencenullchangedmetadata.transactions[35].transaction_type.ocrConfidence0.993changedmetadata.transactions[36].amount.logprobsConfidencenullchangedmetadata.transactions[36].amount.ocrConfidence0.995changedmetadata.transactions[36].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[36].balance_after_transaction.ocrConfidence0.998changedmetadata.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…changedmetadata.transactions[36].description.logprobsConfidencenullchangedmetadata.transactions[36].description.ocrConfidence0.985changedmetadata.transactions[36].logprobsConfidencenullchangedmetadata.transactions[36].nsf_related.logprobsConfidencenullchangedmetadata.transactions[36].ocrConfidence0.985changedmetadata.transactions[36].transaction_date.logprobsConfidencenullchangedmetadata.transactions[36].transaction_date.ocrConfidence0.991changedmetadata.transactions[36].transaction_type.logprobsConfidencenullchangedmetadata.transactions[36].transaction_type.ocrConfidence0.993changedmetadata.transactions[37].amount.logprobsConfidencenullchangedmetadata.transactions[37].amount.ocrConfidence0.996changedmetadata.transactions[37].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[37].balance_after_transaction.ocrConfidence0.999changedmetadata.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":…changedmetadata.transactions[37].description.logprobsConfidencenullchangedmetadata.transactions[37].description.ocrConfidence0.837changedmetadata.transactions[37].logprobsConfidencenullchangedmetadata.transactions[37].nsf_related.logprobsConfidencenullchangedmetadata.transactions[37].ocrConfidence0.837changedmetadata.transactions[37].transaction_date.logprobsConfidencenullchangedmetadata.transactions[37].transaction_date.ocrConfidence0.991changedmetadata.transactions[37].transaction_type.logprobsConfidencenullchangedmetadata.transactions[37].transaction_type.ocrConfidence0.993changedmetadata.transactions[38].amount.logprobsConfidencenullchangedmetadata.transactions[38].amount.ocrConfidence0.992changedmetadata.transactions[38].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[38].balance_after_transaction.ocrConfidence0.998changedmetadata.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…changedmetadata.transactions[38].description.logprobsConfidencenullchangedmetadata.transactions[38].description.ocrConfidence0.964changedmetadata.transactions[38].logprobsConfidencenullchangedmetadata.transactions[38].nsf_related.logprobsConfidencenullchangedmetadata.transactions[38].ocrConfidence0.964changedmetadata.transactions[38].transaction_date.logprobsConfidencenullchangedmetadata.transactions[38].transaction_date.ocrConfidence0.991changedmetadata.transactions[38].transaction_type.logprobsConfidencenullchangedmetadata.transactions[38].transaction_type.ocrConfidence0.992changedmetadata.transactions[39].amount.logprobsConfidencenullchangedmetadata.transactions[39].amount.ocrConfidence0.983changedmetadata.transactions[39].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[39].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[39].description.logprobsConfidencenullchangedmetadata.transactions[39].description.ocrConfidence0.964changedmetadata.transactions[39].logprobsConfidencenullchangedmetadata.transactions[39].nsf_related.logprobsConfidencenullchangedmetadata.transactions[39].ocrConfidence0.964changedmetadata.transactions[39].transaction_date.logprobsConfidencenullchangedmetadata.transactions[39].transaction_date.ocrConfidence0.991changedmetadata.transactions[39].transaction_type.logprobsConfidencenullchangedmetadata.transactions[39].transaction_type.ocrConfidence0.992changedmetadata.transactions[3].amount.logprobsConfidencenullchangedmetadata.transactions[3].amount.ocrConfidence0.994changedmetadata.transactions[3].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[3].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[3].description.logprobsConfidencenullchangedmetadata.transactions[3].description.ocrConfidence0.988changedmetadata.transactions[3].logprobsConfidencenullchangedmetadata.transactions[3].nsf_related.logprobsConfidencenullchangedmetadata.transactions[3].ocrConfidence0.988changedmetadata.transactions[3].transaction_date.logprobsConfidencenullchangedmetadata.transactions[3].transaction_date.ocrConfidence0.99changedmetadata.transactions[3].transaction_type.logprobsConfidencenullchangedmetadata.transactions[3].transaction_type.ocrConfidence0.995changedmetadata.transactions[40].amount.logprobsConfidencenullchangedmetadata.transactions[40].amount.ocrConfidence0.994changedmetadata.transactions[40].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[40].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[40].description.logprobsConfidencenullchangedmetadata.transactions[40].description.ocrConfidence0.985changedmetadata.transactions[40].logprobsConfidencenullchangedmetadata.transactions[40].nsf_related.logprobsConfidencenullchangedmetadata.transactions[40].ocrConfidence0.985changedmetadata.transactions[40].transaction_date.logprobsConfidencenullchangedmetadata.transactions[40].transaction_date.ocrConfidence0.991changedmetadata.transactions[40].transaction_type.logprobsConfidencenullchangedmetadata.transactions[40].transaction_type.ocrConfidence0.993changedmetadata.transactions[41].amount.logprobsConfidencenullchangedmetadata.transactions[41].amount.ocrConfidence0.993changedmetadata.transactions[41].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[41].balance_after_transaction.ocrConfidence0.998changedmetadata.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":…changedmetadata.transactions[41].description.logprobsConfidencenullchangedmetadata.transactions[41].description.ocrConfidence0.989changedmetadata.transactions[41].logprobsConfidencenullchangedmetadata.transactions[41].nsf_related.logprobsConfidencenullchangedmetadata.transactions[41].ocrConfidence0.989changedmetadata.transactions[41].transaction_date.logprobsConfidencenullchangedmetadata.transactions[41].transaction_date.ocrConfidence0.991changedmetadata.transactions[41].transaction_type.logprobsConfidencenullchangedmetadata.transactions[41].transaction_type.ocrConfidence0.994changedmetadata.transactions[42].amount.logprobsConfidencenullchangedmetadata.transactions[42].amount.ocrConfidence0.993changedmetadata.transactions[42].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[42].balance_after_transaction.ocrConfidence0.996changedmetadata.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…changedmetadata.transactions[42].description.logprobsConfidencenullchangedmetadata.transactions[42].description.ocrConfidence0.973changedmetadata.transactions[42].logprobsConfidencenullchangedmetadata.transactions[42].nsf_related.logprobsConfidencenullchangedmetadata.transactions[42].ocrConfidence0.973changedmetadata.transactions[42].transaction_date.logprobsConfidencenullchangedmetadata.transactions[42].transaction_date.ocrConfidence0.991changedmetadata.transactions[42].transaction_type.logprobsConfidencenullchangedmetadata.transactions[42].transaction_type.ocrConfidence0.993changedmetadata.transactions[4].amount.logprobsConfidencenullchangedmetadata.transactions[4].amount.ocrConfidence0.995changedmetadata.transactions[4].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[4].balance_after_transaction.ocrConfidence0.997changedmetadata.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}],"…changedmetadata.transactions[4].description.logprobsConfidencenullchangedmetadata.transactions[4].description.ocrConfidence0.988changedmetadata.transactions[4].logprobsConfidencenullchangedmetadata.transactions[4].nsf_related.logprobsConfidencenullchangedmetadata.transactions[4].ocrConfidence0.988changedmetadata.transactions[4].transaction_date.logprobsConfidencenullchangedmetadata.transactions[4].transaction_date.ocrConfidence0.99changedmetadata.transactions[4].transaction_type.logprobsConfidencenullchangedmetadata.transactions[4].transaction_type.ocrConfidence0.993changedmetadata.transactions[5].amount.logprobsConfidencenullchangedmetadata.transactions[5].amount.ocrConfidence0.996changedmetadata.transactions[5].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[5].balance_after_transaction.ocrConfidence0.996changedmetadata.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.…changedmetadata.transactions[5].description.logprobsConfidencenullchangedmetadata.transactions[5].description.ocrConfidence0.988changedmetadata.transactions[5].logprobsConfidencenullchangedmetadata.transactions[5].nsf_related.logprobsConfidencenullchangedmetadata.transactions[5].ocrConfidence0.988changedmetadata.transactions[5].transaction_date.logprobsConfidencenullchangedmetadata.transactions[5].transaction_date.ocrConfidence0.99changedmetadata.transactions[5].transaction_type.logprobsConfidencenullchangedmetadata.transactions[5].transaction_type.ocrConfidence0.993changedmetadata.transactions[6].amount.logprobsConfidencenullchangedmetadata.transactions[6].amount.ocrConfidence0.995changedmetadata.transactions[6].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[6].balance_after_transaction.ocrConfidence0.997changedmetadata.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…changedmetadata.transactions[6].description.logprobsConfidencenullchangedmetadata.transactions[6].description.ocrConfidence0.977changedmetadata.transactions[6].logprobsConfidencenullchangedmetadata.transactions[6].nsf_related.logprobsConfidencenullchangedmetadata.transactions[6].ocrConfidence0.977changedmetadata.transactions[6].transaction_date.logprobsConfidencenullchangedmetadata.transactions[6].transaction_date.ocrConfidence0.99changedmetadata.transactions[6].transaction_type.logprobsConfidencenullchangedmetadata.transactions[6].transaction_type.ocrConfidence0.993changedmetadata.transactions[7].amount.logprobsConfidencenullchangedmetadata.transactions[7].amount.ocrConfidence0.995changedmetadata.transactions[7].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[7].balance_after_transaction.ocrConfidence0.977changedmetadata.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…changedmetadata.transactions[7].description.logprobsConfidencenullchangedmetadata.transactions[7].description.ocrConfidence0.977changedmetadata.transactions[7].logprobsConfidencenullchangedmetadata.transactions[7].nsf_related.logprobsConfidencenullchangedmetadata.transactions[7].ocrConfidence0.977changedmetadata.transactions[7].transaction_date.logprobsConfidencenullchangedmetadata.transactions[7].transaction_date.ocrConfidence0.99changedmetadata.transactions[7].transaction_type.logprobsConfidencenullchangedmetadata.transactions[7].transaction_type.ocrConfidence0.993changedmetadata.transactions[8].amount.logprobsConfidencenullchangedmetadata.transactions[8].amount.ocrConfidence0.998changedmetadata.transactions[8].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[8].balance_after_transaction.ocrConfidence0.997changedmetadata.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"…changedmetadata.transactions[8].description.logprobsConfidencenullchangedmetadata.transactions[8].description.ocrConfidence0.931changedmetadata.transactions[8].logprobsConfidencenullchangedmetadata.transactions[8].nsf_related.logprobsConfidencenullchangedmetadata.transactions[8].ocrConfidence0.931changedmetadata.transactions[8].transaction_date.logprobsConfidencenullchangedmetadata.transactions[8].transaction_date.ocrConfidence0.99changedmetadata.transactions[8].transaction_type.logprobsConfidencenullchangedmetadata.transactions[8].transaction_type.ocrConfidence0.994changedmetadata.transactions[9].amount.logprobsConfidencenullchangedmetadata.transactions[9].amount.ocrConfidence0.995changedmetadata.transactions[9].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[9].balance_after_transaction.ocrConfidence0.994changedmetadata.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…changedmetadata.transactions[9].description.logprobsConfidencenullchangedmetadata.transactions[9].description.ocrConfidence0.988changedmetadata.transactions[9].logprobsConfidencenullchangedmetadata.transactions[9].nsf_related.logprobsConfidencenullchangedmetadata.transactions[9].ocrConfidence0.988changedmetadata.transactions[9].transaction_date.logprobsConfidencenullchangedmetadata.transactions[9].transaction_date.ocrConfidence0.989changedmetadata.transactions[9].transaction_type.logprobsConfidencenullchangedmetadata.transactions[9].transaction_type.ocrConfidence0.993changedvalue.account_holder_name"ANGIE M LOPEZ OR JUAN L LOPEZ"changedvalue.account_number"000000861006133"changedvalue.bank_name"JPMorgan Chase Bank, N.A."changedvalue.beginning_balance.amount438.59changedvalue.beginning_balance.iso_4217_currency_code"USD"changedvalue.ending_balance.amount459.25changedvalue.ending_balance.iso_4217_currency_code"USD"changedvalue.nsf_flagfalsechangedvalue.statement_period_end_date"2021-01-25"changedvalue.statement_period_start_date"2020-12-22"changedvalue.total_deposits.amount3098.99changedvalue.total_deposits.iso_4217_currency_code"USD"changedvalue.total_fees.amount25changedvalue.total_fees.iso_4217_currency_code"USD"changedvalue.total_withdrawals.amount3053.33changedvalue.total_withdrawals.iso_4217_currency_code"USD"changedvalue.transactions[{"amount":-27.9,"description":"Discover E-Payment 6930 Web ID: 2510020270","nsf_related":false,"transaction_date":"2020-12-24","transaction_type":"payment","ba…changedYou can learn more about Extract configuration in Extend's Extract documentation.
{
"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.")
}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.