Extracts account summaries, transactions, and balances from bank statements.
A bank statement is a periodic financial document issued by a financial institution that records all account activity, including opening and closing balances, deposits, withdrawals, fees, and a detailed transaction history for a specified period. This template takes in Bank Statement and outputs markdown (.md) capturing the statement's full text and layout, and JSON (.json) with structured account and transaction fields including bank details, balance information, fee totals, and individual transaction records per the extraction schema by using Extend's Parse, Extract primitives.
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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":55.0872,"y":141.012},{"x":126.96480000000001,"y":141.0552},{"x":126.95760000000001,"y":150.7104…changedmetadata.account_holder_name.logprobsConfidencenullchangedmetadata.account_holder_name.ocrConfidence0.992changedmetadata.account_number.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":361.08,"y":60.0192},{"x":422.5824,"y":60.0264},{"x":422.5824,"y":68.3928},{"x":361.08,"y":68.38…changedmetadata.account_number.logprobsConfidencenullchangedmetadata.account_number.ocrConfidence0.992changedmetadata.account_type.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":202.9176,"y":381.0528},{"x":292.9608,"y":381.4056},{"x":292.9104,"y":392.88239999999996},{"x":2…changedmetadata.account_type.logprobsConfidencenullchangedmetadata.account_type.ocrConfidence0.991changedmetadata.bank_name.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":58.2768,"y":54.309599999999996},{"x":157.7736,"y":54.2016},{"x":157.7808,"y":62.906400000000005…changedmetadata.bank_name.logprobsConfidencenullchangedmetadata.bank_name.ocrConfidence0.993changedmetadata.branch_address.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":58.176,"y":63.5616},{"x":115.27199999999999,"y":63.684},{"x":115.2576,"y":71.8776},{"x":58.1543…changedmetadata.branch_address.logprobsConfidencenullchangedmetadata.branch_address.ocrConfidence0.086changedmetadata.closing_balance.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":39.0168,"y":480.2832},{"x":108.4536,"y":480.3768},{"x":108.4392,"y":490.6656},{"x":39.002399999…changedmetadata.closing_balance.logprobsConfidencenullchangedmetadata.closing_balance.ocrConfidence0.995changedmetadata.opening_balance.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":38.9736,"y":413.532},{"x":121.9248,"y":413.5608},{"x":121.9248,"y":424.46880000000004},{"x":38.…changedmetadata.opening_balance.logprobsConfidencenullchangedmetadata.opening_balance.ocrConfidence0.995changedmetadata.statement_issue_date.citations[]changedmetadata.statement_issue_date.logprobsConfidencenullchangedmetadata.statement_period_end_date.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":359.8488,"y":46.360800000000005},{"x":519.7968000000001,"y":46.3824},{"x":519.7968000000001,"y"…changedmetadata.statement_period_end_date.logprobsConfidencenullchangedmetadata.statement_period_end_date.ocrConfidence0.989changedmetadata.statement_period_start_date.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":359.8488,"y":46.360800000000005},{"x":519.7968000000001,"y":46.3824},{"x":519.7968000000001,"y"…changedmetadata.statement_period_start_date.logprobsConfidencenullchangedmetadata.statement_period_start_date.ocrConfidence0.99changedmetadata.total_credits.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":39.1104,"y":426.96},{"x":133.1568,"y":426.9168},{"x":133.164,"y":437.6592},{"x":39.1104,"y":437…changedmetadata.total_credits.logprobsConfidencenullchangedmetadata.total_credits.ocrConfidence0.992changedmetadata.total_debits.citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":39.139199999999995,"y":439.7832},{"x":166.1472,"y":439.86240000000004},{"x":166.14000000000001,…changedmetadata.total_debits.logprobsConfidencenullchangedmetadata.total_debits.ocrConfidence0.989changedmetadata.transactions.citations[]changedmetadata.transactions.logprobsConfidencenullchangedmetadata.transactions.ocrConfidence0.83changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.25920000000001,"y":563.4864},{"x":59.5368,"y":563.508},{"x":59.5224,"y":572.148},{"x":36.244…changedmetadata.transactions[0].description.logprobsConfidencenullchangedmetadata.transactions[0].description.ocrConfidence0.985changedmetadata.transactions[0].logprobsConfidencenullchangedmetadata.transactions[0].ocrConfidence0.985changedmetadata.transactions[0].reference_number.logprobsConfidencenullchangedmetadata.transactions[0].reference_number.ocrConfidence0.995changedmetadata.transactions[0].transaction_date.logprobsConfidencenullchangedmetadata.transactions[0].transaction_date.ocrConfidence0.99changedmetadata.transactions[0].transaction_type.logprobsConfidencenullchangedmetadata.transactions[0].transaction_type.ocrConfidence0.993changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":35.6616,"y":723.5136},{"x":59.6232,"y":723.5136},{"x":59.6232,"y":731.8368},{"x":35.6616,"y":73…changedmetadata.transactions[10].description.logprobsConfidencenullchangedmetadata.transactions[10].description.ocrConfidence0.988changedmetadata.transactions[10].logprobsConfidencenullchangedmetadata.transactions[10].ocrConfidence0.988changedmetadata.transactions[10].reference_number.citations[]changedmetadata.transactions[10].reference_number.logprobsConfidencenullchangedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.7176,"y":130.0464},{"x":57.5568,"y":130.1184},{"x":57.5352,"y":139.0032},{"x":33.6888,"y":13…changedmetadata.transactions[11].description.logprobsConfidencenullchangedmetadata.transactions[11].description.ocrConfidence0.985changedmetadata.transactions[11].logprobsConfidencenullchangedmetadata.transactions[11].ocrConfidence0.985changedmetadata.transactions[11].reference_number.citations[]changedmetadata.transactions[11].reference_number.logprobsConfidencenullchangedmetadata.transactions[11].transaction_date.logprobsConfidencenullchangedmetadata.transactions[11].transaction_date.ocrConfidence0.991changedmetadata.transactions[11].transaction_type.logprobsConfidencenullchangedmetadata.transactions[11].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.588,"y":142.4016},{"x":57.5064,"y":142.4448},{"x":57.492,"y":151.3944},{"x":33.5736,"y":151.…changedmetadata.transactions[12].description.logprobsConfidencenullchangedmetadata.transactions[12].description.ocrConfidence0.9changedmetadata.transactions[12].logprobsConfidencenullchangedmetadata.transactions[12].ocrConfidence0.9changedmetadata.transactions[12].reference_number.citations[]changedmetadata.transactions[12].reference_number.logprobsConfidencenullchangedmetadata.transactions[12].transaction_date.logprobsConfidencenullchangedmetadata.transactions[12].transaction_date.ocrConfidence0.991changedmetadata.transactions[12].transaction_type.logprobsConfidencenullchangedmetadata.transactions[12].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6168,"y":154.7424},{"x":57.132,"y":154.74960000000002},{"x":57.132,"y":163.7424},{"x":33.616…changedmetadata.transactions[13].description.logprobsConfidencenullchangedmetadata.transactions[13].description.ocrConfidence0.916changedmetadata.transactions[13].logprobsConfidencenullchangedmetadata.transactions[13].ocrConfidence0.916changedmetadata.transactions[13].reference_number.logprobsConfidencenullchangedmetadata.transactions[13].reference_number.ocrConfidence0.995changedmetadata.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.998changedmetadata.transactions[14].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[14].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[14].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6096,"y":167.148},{"x":57.1248,"y":167.1624},{"x":57.1176,"y":176.0616},{"x":33.6024,"y":176…changedmetadata.transactions[14].description.logprobsConfidencenullchangedmetadata.transactions[14].description.ocrConfidence0.964changedmetadata.transactions[14].logprobsConfidencenullchangedmetadata.transactions[14].ocrConfidence0.964changedmetadata.transactions[14].reference_number.logprobsConfidencenullchangedmetadata.transactions[14].reference_number.ocrConfidence0.994changedmetadata.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.995changedmetadata.transactions[15].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[15].balance_after_transaction.ocrConfidence0.995changedmetadata.transactions[15].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5376,"y":179.0136},{"x":57.088800000000006,"y":179.07840000000002},{"x":57.0672,"y":188.1720…changedmetadata.transactions[15].description.logprobsConfidencenullchangedmetadata.transactions[15].description.ocrConfidence0.881changedmetadata.transactions[15].logprobsConfidencenullchangedmetadata.transactions[15].ocrConfidence0.881changedmetadata.transactions[15].reference_number.citations[]changedmetadata.transactions[15].reference_number.logprobsConfidencenullchangedmetadata.transactions[15].transaction_date.logprobsConfidencenullchangedmetadata.transactions[15].transaction_date.ocrConfidence0.991changedmetadata.transactions[15].transaction_type.logprobsConfidencenullchangedmetadata.transactions[15].transaction_type.ocrConfidence0.992changedmetadata.transactions[16].amount.logprobsConfidencenullchangedmetadata.transactions[16].amount.ocrConfidence0.996changedmetadata.transactions[16].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[16].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[16].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.552,"y":200.49120000000002},{"x":57.002399999999994,"y":200.4984},{"x":57.002399999999994,"y…changedmetadata.transactions[16].description.logprobsConfidencenullchangedmetadata.transactions[16].description.ocrConfidence0.985changedmetadata.transactions[16].logprobsConfidencenullchangedmetadata.transactions[16].ocrConfidence0.985changedmetadata.transactions[16].reference_number.citations[]changedmetadata.transactions[16].reference_number.logprobsConfidencenullchangedmetadata.transactions[16].transaction_date.logprobsConfidencenullchangedmetadata.transactions[16].transaction_date.ocrConfidence0.991changedmetadata.transactions[16].transaction_type.logprobsConfidencenullchangedmetadata.transactions[16].transaction_type.ocrConfidence0.992changedmetadata.transactions[17].amount.logprobsConfidencenullchangedmetadata.transactions[17].amount.ocrConfidence0.997changedmetadata.transactions[17].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[17].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[17].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6744,"y":212.88240000000002},{"x":56.937599999999996,"y":212.8896},{"x":56.937599999999996,"…changedmetadata.transactions[17].description.logprobsConfidencenullchangedmetadata.transactions[17].description.ocrConfidence0.964changedmetadata.transactions[17].logprobsConfidencenullchangedmetadata.transactions[17].ocrConfidence0.964changedmetadata.transactions[17].reference_number.logprobsConfidencenullchangedmetadata.transactions[17].reference_number.ocrConfidence0.995changedmetadata.transactions[17].transaction_date.logprobsConfidencenullchangedmetadata.transactions[17].transaction_date.ocrConfidence0.991changedmetadata.transactions[17].transaction_type.logprobsConfidencenullchangedmetadata.transactions[17].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.580799999999996,"y":225.3672},{"x":55.872,"y":225.37439999999998},{"x":55.872,"y":234.1944},…changedmetadata.transactions[18].description.logprobsConfidencenullchangedmetadata.transactions[18].description.ocrConfidence0.985changedmetadata.transactions[18].logprobsConfidencenullchangedmetadata.transactions[18].ocrConfidence0.985changedmetadata.transactions[18].reference_number.citations[]changedmetadata.transactions[18].reference_number.logprobsConfidencenullchangedmetadata.transactions[18].transaction_date.logprobsConfidencenullchangedmetadata.transactions[18].transaction_date.ocrConfidence0.991changedmetadata.transactions[18].transaction_type.logprobsConfidencenullchangedmetadata.transactions[18].transaction_type.ocrConfidence0.992changedmetadata.transactions[19].amount.logprobsConfidencenullchangedmetadata.transactions[19].amount.ocrConfidence0.996changedmetadata.transactions[19].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[19].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[19].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4368,"y":246.8016},{"x":55.9584,"y":246.8376},{"x":55.944,"y":255.7728},{"x":33.4224,"y":255…changedmetadata.transactions[19].description.logprobsConfidencenullchangedmetadata.transactions[19].description.ocrConfidence0.975changedmetadata.transactions[19].logprobsConfidencenullchangedmetadata.transactions[19].ocrConfidence0.975changedmetadata.transactions[19].reference_number.citations[]changedmetadata.transactions[19].reference_number.logprobsConfidencenullchangedmetadata.transactions[19].transaction_date.logprobsConfidencenullchangedmetadata.transactions[19].transaction_date.ocrConfidence0.991changedmetadata.transactions[19].transaction_type.logprobsConfidencenullchangedmetadata.transactions[19].transaction_type.ocrConfidence0.992changedmetadata.transactions[1].amount.logprobsConfidencenullchangedmetadata.transactions[1].amount.ocrConfidence0.995changedmetadata.transactions[1].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[1].balance_after_transaction.ocrConfidence0.997changedmetadata.transactions[1].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.403200000000005,"y":575.9136},{"x":59.0976,"y":576.0144},{"x":59.0616,"y":584.496},{"x":36.3…changedmetadata.transactions[1].description.logprobsConfidencenullchangedmetadata.transactions[1].description.ocrConfidence0.989changedmetadata.transactions[1].logprobsConfidencenullchangedmetadata.transactions[1].ocrConfidence0.989changedmetadata.transactions[1].reference_number.logprobsConfidencenullchangedmetadata.transactions[1].reference_number.ocrConfidence0.994changedmetadata.transactions[1].transaction_date.logprobsConfidencenullchangedmetadata.transactions[1].transaction_date.ocrConfidence0.99changedmetadata.transactions[1].transaction_type.logprobsConfidencenullchangedmetadata.transactions[1].transaction_type.ocrConfidence0.993changedmetadata.transactions[20].amount.logprobsConfidencenullchangedmetadata.transactions[20].amount.ocrConfidence0.997changedmetadata.transactions[20].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[20].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[20].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6528,"y":268.4088},{"x":57.139199999999995,"y":268.4592},{"x":57.1176,"y":277.416},{"x":33.6…changedmetadata.transactions[20].description.logprobsConfidencenullchangedmetadata.transactions[20].description.ocrConfidence0.956changedmetadata.transactions[20].logprobsConfidencenullchangedmetadata.transactions[20].ocrConfidence0.956changedmetadata.transactions[20].reference_number.logprobsConfidencenullchangedmetadata.transactions[20].reference_number.ocrConfidence0.993changedmetadata.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.995changedmetadata.transactions[21].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[21].balance_after_transaction.ocrConfidence0.995changedmetadata.transactions[21].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.696000000000005,"y":280.7136},{"x":57.132,"y":280.84319999999997},{"x":57.081599999999995,"y…changedmetadata.transactions[21].description.logprobsConfidencenullchangedmetadata.transactions[21].description.ocrConfidence0.964changedmetadata.transactions[21].logprobsConfidencenullchangedmetadata.transactions[21].ocrConfidence0.964changedmetadata.transactions[21].reference_number.logprobsConfidencenullchangedmetadata.transactions[21].reference_number.ocrConfidence0.994changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.7248,"y":293.09040000000005},{"x":57.1176,"y":293.1768},{"x":57.088800000000006,"y":302.0616…changedmetadata.transactions[22].description.logprobsConfidencenullchangedmetadata.transactions[22].description.ocrConfidence0.916changedmetadata.transactions[22].logprobsConfidencenullchangedmetadata.transactions[22].ocrConfidence0.916changedmetadata.transactions[22].reference_number.logprobsConfidencenullchangedmetadata.transactions[22].reference_number.ocrConfidence0.995changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.7032,"y":304.99920000000003},{"x":57.0312,"y":305.0712},{"x":57.002399999999994,"y":314.0784…changedmetadata.transactions[23].description.logprobsConfidencenullchangedmetadata.transactions[23].description.ocrConfidence0.916changedmetadata.transactions[23].logprobsConfidencenullchangedmetadata.transactions[23].ocrConfidence0.916changedmetadata.transactions[23].reference_number.citations[]changedmetadata.transactions[23].reference_number.logprobsConfidencenullchangedmetadata.transactions[23].transaction_date.logprobsConfidencenullchangedmetadata.transactions[23].transaction_date.ocrConfidence0.991changedmetadata.transactions[23].transaction_type.logprobsConfidencenullchangedmetadata.transactions[23].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.732,"y":317.34},{"x":57.06,"y":317.3544},{"x":57.0528,"y":326.41200000000003},{"x":33.7248,"…changedmetadata.transactions[24].description.logprobsConfidencenullchangedmetadata.transactions[24].description.ocrConfidence0.916changedmetadata.transactions[24].logprobsConfidencenullchangedmetadata.transactions[24].ocrConfidence0.916changedmetadata.transactions[24].reference_number.citations[]changedmetadata.transactions[24].reference_number.logprobsConfidencenullchangedmetadata.transactions[24].transaction_date.logprobsConfidencenullchangedmetadata.transactions[24].transaction_date.ocrConfidence0.991changedmetadata.transactions[24].transaction_type.logprobsConfidencenullchangedmetadata.transactions[24].transaction_type.ocrConfidence0.992changedmetadata.transactions[25].amount.logprobsConfidencenullchangedmetadata.transactions[25].amount.ocrConfidence0.996changedmetadata.transactions[25].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[25].balance_after_transaction.ocrConfidence0.967changedmetadata.transactions[25].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6024,"y":329.61600000000004},{"x":57.3264,"y":329.688},{"x":57.297599999999996,"y":338.88239…changedmetadata.transactions[25].description.logprobsConfidencenullchangedmetadata.transactions[25].description.ocrConfidence0.959changedmetadata.transactions[25].logprobsConfidencenullchangedmetadata.transactions[25].ocrConfidence0.959changedmetadata.transactions[25].reference_number.logprobsConfidencenullchangedmetadata.transactions[25].reference_number.ocrConfidence0.992changedmetadata.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.992changedmetadata.transactions[26].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.544799999999995,"y":379.08720000000005},{"x":57.4344,"y":379.1088},{"x":57.42,"y":388.116000…changedmetadata.transactions[26].description.logprobsConfidencenullchangedmetadata.transactions[26].description.ocrConfidence0.984changedmetadata.transactions[26].logprobsConfidencenullchangedmetadata.transactions[26].ocrConfidence0.984changedmetadata.transactions[26].reference_number.citations[]changedmetadata.transactions[26].reference_number.logprobsConfidencenullchangedmetadata.transactions[26].transaction_date.logprobsConfidencenullchangedmetadata.transactions[26].transaction_date.ocrConfidence0.991changedmetadata.transactions[26].transaction_type.logprobsConfidencenullchangedmetadata.transactions[26].transaction_type.ocrConfidence0.992changedmetadata.transactions[27].amount.logprobsConfidencenullchangedmetadata.transactions[27].amount.ocrConfidence0.995changedmetadata.transactions[27].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[27].balance_after_transaction.ocrConfidence0.992changedmetadata.transactions[27].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5376,"y":400.92479999999995},{"x":57.4632,"y":400.9392},{"x":57.456,"y":410.09040000000005},…changedmetadata.transactions[27].description.logprobsConfidencenullchangedmetadata.transactions[27].description.ocrConfidence0.881changedmetadata.transactions[27].logprobsConfidencenullchangedmetadata.transactions[27].ocrConfidence0.881changedmetadata.transactions[27].reference_number.citations[]changedmetadata.transactions[27].reference_number.logprobsConfidencenullchangedmetadata.transactions[27].transaction_date.logprobsConfidencenullchangedmetadata.transactions[27].transaction_date.ocrConfidence0.991changedmetadata.transactions[27].transaction_type.logprobsConfidencenullchangedmetadata.transactions[27].transaction_type.ocrConfidence0.992changedmetadata.transactions[28].amount.logprobsConfidencenullchangedmetadata.transactions[28].amount.ocrConfidence0.995changedmetadata.transactions[28].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[28].balance_after_transaction.ocrConfidence0.992changedmetadata.transactions[28].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.3576,"y":422.5608},{"x":57.355199999999996,"y":422.48879999999997},{"x":57.3768,"y":431.7335…changedmetadata.transactions[28].description.logprobsConfidencenullchangedmetadata.transactions[28].description.ocrConfidence0.881changedmetadata.transactions[28].logprobsConfidencenullchangedmetadata.transactions[28].ocrConfidence0.881changedmetadata.transactions[28].reference_number.citations[]changedmetadata.transactions[28].reference_number.logprobsConfidencenullchangedmetadata.transactions[28].transaction_date.logprobsConfidencenullchangedmetadata.transactions[28].transaction_date.ocrConfidence0.991changedmetadata.transactions[28].transaction_type.logprobsConfidencenullchangedmetadata.transactions[28].transaction_type.ocrConfidence0.992changedmetadata.transactions[29].amount.logprobsConfidencenullchangedmetadata.transactions[29].amount.ocrConfidence0.995changedmetadata.transactions[29].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[29].balance_after_transaction.ocrConfidence0.992changedmetadata.transactions[29].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4728,"y":443.9448},{"x":57.448800000000006,"y":443.8872},{"x":57.4704,"y":453.22560000000004…changedmetadata.transactions[29].description.logprobsConfidencenullchangedmetadata.transactions[29].description.ocrConfidence0.83changedmetadata.transactions[29].logprobsConfidencenullchangedmetadata.transactions[29].ocrConfidence0.83changedmetadata.transactions[29].reference_number.logprobsConfidencenullchangedmetadata.transactions[29].reference_number.ocrConfidence0.99changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.1512,"y":588.132},{"x":59.0976,"y":588.1536},{"x":59.0904,"y":596.8008},{"x":36.1368,"y":596…changedmetadata.transactions[2].description.logprobsConfidencenullchangedmetadata.transactions[2].description.ocrConfidence0.977changedmetadata.transactions[2].logprobsConfidencenullchangedmetadata.transactions[2].ocrConfidence0.977changedmetadata.transactions[2].reference_number.citations[]changedmetadata.transactions[2].reference_number.logprobsConfidencenullchangedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.588,"y":475.2},{"x":57.355199999999996,"y":475.2},{"x":57.355199999999996,"y":484.3008},{"x"…changedmetadata.transactions[30].description.logprobsConfidencenullchangedmetadata.transactions[30].description.ocrConfidence0.964changedmetadata.transactions[30].logprobsConfidencenullchangedmetadata.transactions[30].ocrConfidence0.964changedmetadata.transactions[30].reference_number.logprobsConfidencenullchangedmetadata.transactions[30].reference_number.ocrConfidence0.995changedmetadata.transactions[30].transaction_date.logprobsConfidencenullchangedmetadata.transactions[30].transaction_date.ocrConfidence0.991changedmetadata.transactions[30].transaction_type.logprobsConfidencenullchangedmetadata.transactions[30].transaction_type.ocrConfidence0.992changedmetadata.transactions[31].amount.logprobsConfidencenullchangedmetadata.transactions[31].amount.ocrConfidence0.995changedmetadata.transactions[31].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[31].balance_after_transaction.ocrConfidence0.997changedmetadata.transactions[31].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6456,"y":487.10159999999996},{"x":57.2616,"y":487.1592},{"x":57.232800000000005,"y":496.1952…changedmetadata.transactions[31].description.logprobsConfidencenullchangedmetadata.transactions[31].description.ocrConfidence0.982changedmetadata.transactions[31].logprobsConfidencenullchangedmetadata.transactions[31].ocrConfidence0.982changedmetadata.transactions[31].reference_number.citations[]changedmetadata.transactions[31].reference_number.logprobsConfidencenullchangedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4584,"y":499.41360000000003},{"x":57.0672,"y":499.4064},{"x":57.0672,"y":508.6944},{"x":33.4…changedmetadata.transactions[32].description.logprobsConfidencenullchangedmetadata.transactions[32].description.ocrConfidence0.881changedmetadata.transactions[32].logprobsConfidencenullchangedmetadata.transactions[32].ocrConfidence0.881changedmetadata.transactions[32].reference_number.citations[]changedmetadata.transactions[32].reference_number.logprobsConfidencenullchangedmetadata.transactions[32].transaction_date.logprobsConfidencenullchangedmetadata.transactions[32].transaction_date.ocrConfidence0.991changedmetadata.transactions[32].transaction_type.logprobsConfidencenullchangedmetadata.transactions[32].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.4584,"y":520.8912},{"x":57.132,"y":520.9056},{"x":57.1248,"y":530.0568000000001},{"x":33.451…changedmetadata.transactions[33].description.logprobsConfidencenullchangedmetadata.transactions[33].description.ocrConfidence0.977changedmetadata.transactions[33].logprobsConfidencenullchangedmetadata.transactions[33].ocrConfidence0.977changedmetadata.transactions[33].reference_number.citations[]changedmetadata.transactions[33].reference_number.logprobsConfidencenullchangedmetadata.transactions[33].transaction_date.logprobsConfidencenullchangedmetadata.transactions[33].transaction_date.ocrConfidence0.991changedmetadata.transactions[33].transaction_type.logprobsConfidencenullchangedmetadata.transactions[33].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.544799999999995,"y":542.484},{"x":56.9088,"y":542.556},{"x":56.88,"y":551.6496000000001},{"x…changedmetadata.transactions[34].description.logprobsConfidencenullchangedmetadata.transactions[34].description.ocrConfidence0.985changedmetadata.transactions[34].logprobsConfidencenullchangedmetadata.transactions[34].ocrConfidence0.985changedmetadata.transactions[34].reference_number.citations[]changedmetadata.transactions[34].reference_number.logprobsConfidencenullchangedmetadata.transactions[34].transaction_date.logprobsConfidencenullchangedmetadata.transactions[34].transaction_date.ocrConfidence0.991changedmetadata.transactions[34].transaction_type.logprobsConfidencenullchangedmetadata.transactions[34].transaction_type.ocrConfidence0.992changedmetadata.transactions[35].amount.logprobsConfidencenullchangedmetadata.transactions[35].amount.ocrConfidence0.997changedmetadata.transactions[35].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[35].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[35].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5736,"y":554.7528},{"x":56.9592,"y":554.8464},{"x":56.923199999999994,"y":563.9976},{"x":33.…changedmetadata.transactions[35].description.logprobsConfidencenullchangedmetadata.transactions[35].description.ocrConfidence0.985changedmetadata.transactions[35].logprobsConfidencenullchangedmetadata.transactions[35].ocrConfidence0.985changedmetadata.transactions[35].reference_number.citations[]changedmetadata.transactions[35].reference_number.logprobsConfidencenullchangedmetadata.transactions[35].transaction_date.logprobsConfidencenullchangedmetadata.transactions[35].transaction_date.ocrConfidence0.991changedmetadata.transactions[35].transaction_type.logprobsConfidencenullchangedmetadata.transactions[35].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6168,"y":567.288},{"x":56.8224,"y":567.3672},{"x":56.7936,"y":576.2520000000001},{"x":33.588…changedmetadata.transactions[36].description.logprobsConfidencenullchangedmetadata.transactions[36].description.ocrConfidence0.985changedmetadata.transactions[36].logprobsConfidencenullchangedmetadata.transactions[36].ocrConfidence0.985changedmetadata.transactions[36].reference_number.citations[]changedmetadata.transactions[36].reference_number.logprobsConfidencenullchangedmetadata.transactions[36].transaction_date.logprobsConfidencenullchangedmetadata.transactions[36].transaction_date.ocrConfidence0.991changedmetadata.transactions[36].transaction_type.logprobsConfidencenullchangedmetadata.transactions[36].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.552,"y":579.2615999999999},{"x":55.98,"y":579.3624},{"x":55.944,"y":588.2760000000001},{"x":…changedmetadata.transactions[37].description.logprobsConfidencenullchangedmetadata.transactions[37].description.ocrConfidence0.837changedmetadata.transactions[37].logprobsConfidencenullchangedmetadata.transactions[37].ocrConfidence0.837changedmetadata.transactions[37].reference_number.citations[]changedmetadata.transactions[37].reference_number.logprobsConfidencenullchangedmetadata.transactions[37].transaction_date.logprobsConfidencenullchangedmetadata.transactions[37].transaction_date.ocrConfidence0.991changedmetadata.transactions[37].transaction_type.logprobsConfidencenullchangedmetadata.transactions[37].transaction_type.ocrConfidence0.992changedmetadata.transactions[38].amount.logprobsConfidencenullchangedmetadata.transactions[38].amount.ocrConfidence0.997changedmetadata.transactions[38].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[38].balance_after_transaction.ocrConfidence0.998changedmetadata.transactions[38].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.588,"y":591.6168},{"x":57.1248,"y":591.7031999999999},{"x":57.088800000000006,"y":600.6384},…changedmetadata.transactions[38].description.logprobsConfidencenullchangedmetadata.transactions[38].description.ocrConfidence0.964changedmetadata.transactions[38].logprobsConfidencenullchangedmetadata.transactions[38].ocrConfidence0.964changedmetadata.transactions[38].reference_number.logprobsConfidencenullchangedmetadata.transactions[38].reference_number.ocrConfidence0.994changedmetadata.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.995changedmetadata.transactions[39].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[39].balance_after_transaction.ocrConfidence0.996changedmetadata.transactions[39].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6024,"y":603.9288},{"x":57.002399999999994,"y":603.972},{"x":56.988,"y":612.9864},{"x":33.58…changedmetadata.transactions[39].description.logprobsConfidencenullchangedmetadata.transactions[39].description.ocrConfidence0.964changedmetadata.transactions[39].logprobsConfidencenullchangedmetadata.transactions[39].ocrConfidence0.964changedmetadata.transactions[39].reference_number.logprobsConfidencenullchangedmetadata.transactions[39].reference_number.ocrConfidence0.994changedmetadata.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.998changedmetadata.transactions[3].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[3].balance_after_transaction.ocrConfidence0.997changedmetadata.transactions[3].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.2736,"y":609.5015999999999},{"x":59.169599999999996,"y":609.6456},{"x":59.119200000000006,"y…changedmetadata.transactions[3].description.logprobsConfidencenullchangedmetadata.transactions[3].description.ocrConfidence0.988changedmetadata.transactions[3].logprobsConfidencenullchangedmetadata.transactions[3].ocrConfidence0.988changedmetadata.transactions[3].reference_number.citations[]changedmetadata.transactions[3].reference_number.logprobsConfidencenullchangedmetadata.transactions[3].transaction_date.logprobsConfidencenullchangedmetadata.transactions[3].transaction_date.ocrConfidence0.99changedmetadata.transactions[3].transaction_type.logprobsConfidencenullchangedmetadata.transactions[3].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6168,"y":616.2983999999999},{"x":57.0384,"y":616.356},{"x":57.0168,"y":625.3704},{"x":33.595…changedmetadata.transactions[40].description.logprobsConfidencenullchangedmetadata.transactions[40].description.ocrConfidence0.985changedmetadata.transactions[40].logprobsConfidencenullchangedmetadata.transactions[40].ocrConfidence0.985changedmetadata.transactions[40].reference_number.citations[]changedmetadata.transactions[40].reference_number.logprobsConfidencenullchangedmetadata.transactions[40].transaction_date.logprobsConfidencenullchangedmetadata.transactions[40].transaction_date.ocrConfidence0.991changedmetadata.transactions[40].transaction_type.logprobsConfidencenullchangedmetadata.transactions[40].transaction_type.ocrConfidence0.992changedmetadata.transactions[41].amount.logprobsConfidencenullchangedmetadata.transactions[41].amount.ocrConfidence0.995changedmetadata.transactions[41].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[41].balance_after_transaction.ocrConfidence0.998changedmetadata.transactions[41].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.6024,"y":628.3223999999999},{"x":57.0096,"y":628.4232},{"x":56.9664,"y":637.3296},{"x":33.56…changedmetadata.transactions[41].description.logprobsConfidencenullchangedmetadata.transactions[41].description.ocrConfidence0.989changedmetadata.transactions[41].logprobsConfidencenullchangedmetadata.transactions[41].ocrConfidence0.989changedmetadata.transactions[41].reference_number.logprobsConfidencenullchangedmetadata.transactions[41].reference_number.ocrConfidence0.992changedmetadata.transactions[41].transaction_date.logprobsConfidencenullchangedmetadata.transactions[41].transaction_date.ocrConfidence0.991changedmetadata.transactions[41].transaction_type.logprobsConfidencenullchangedmetadata.transactions[41].transaction_type.ocrConfidence0.992changedmetadata.transactions[42].amount.logprobsConfidencenullchangedmetadata.transactions[42].amount.ocrConfidence0.995changedmetadata.transactions[42].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[42].balance_after_transaction.ocrConfidence0.995changedmetadata.transactions[42].citations[{"page":2,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":33.5376,"y":640.6200000000001},{"x":57.218399999999995,"y":640.6344},{"x":57.2112,"y":649.5336}…changedmetadata.transactions[42].description.logprobsConfidencenullchangedmetadata.transactions[42].description.ocrConfidence0.973changedmetadata.transactions[42].logprobsConfidencenullchangedmetadata.transactions[42].ocrConfidence0.973changedmetadata.transactions[42].reference_number.citations[]changedmetadata.transactions[42].reference_number.logprobsConfidencenullchangedmetadata.transactions[42].transaction_date.logprobsConfidencenullchangedmetadata.transactions[42].transaction_date.ocrConfidence0.991changedmetadata.transactions[42].transaction_type.logprobsConfidencenullchangedmetadata.transactions[42].transaction_type.ocrConfidence0.992changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.3528,"y":621.8568},{"x":59.0976,"y":621.9504},{"x":59.0616,"y":630.7343999999999},{"x":36.31…changedmetadata.transactions[4].description.logprobsConfidencenullchangedmetadata.transactions[4].description.ocrConfidence0.988changedmetadata.transactions[4].logprobsConfidencenullchangedmetadata.transactions[4].ocrConfidence0.988changedmetadata.transactions[4].reference_number.citations[]changedmetadata.transactions[4].reference_number.logprobsConfidencenullchangedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.3744,"y":633.9888000000001},{"x":59.0112,"y":634.1256},{"x":58.9608,"y":642.8232},{"x":36.31…changedmetadata.transactions[5].description.logprobsConfidencenullchangedmetadata.transactions[5].description.ocrConfidence0.988changedmetadata.transactions[5].logprobsConfidencenullchangedmetadata.transactions[5].ocrConfidence0.988changedmetadata.transactions[5].reference_number.citations[]changedmetadata.transactions[5].reference_number.logprobsConfidencenullchangedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.36,"y":646.3584},{"x":58.953599999999994,"y":646.4448},{"x":58.9176,"y":654.9552},{"x":36.33…changedmetadata.transactions[6].description.logprobsConfidencenullchangedmetadata.transactions[6].description.ocrConfidence0.977changedmetadata.transactions[6].logprobsConfidencenullchangedmetadata.transactions[6].ocrConfidence0.977changedmetadata.transactions[6].reference_number.citations[]changedmetadata.transactions[6].reference_number.logprobsConfidencenullchangedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.230399999999996,"y":667.9943999999999},{"x":59.0688,"y":667.9584},{"x":59.076,"y":676.5264},…changedmetadata.transactions[7].description.logprobsConfidencenullchangedmetadata.transactions[7].description.ocrConfidence0.977changedmetadata.transactions[7].logprobsConfidencenullchangedmetadata.transactions[7].ocrConfidence0.977changedmetadata.transactions[7].reference_number.citations[]changedmetadata.transactions[7].reference_number.logprobsConfidencenullchangedmetadata.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.992changedmetadata.transactions[8].balance_after_transaction.logprobsConfidencenullchangedmetadata.transactions[8].balance_after_transaction.ocrConfidence0.997changedmetadata.transactions[8].citations[{"page":1,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":36.1296,"y":689.4144},{"x":57.9096,"y":689.364},{"x":57.9312,"y":698.0616},{"x":36.1512,"y":698…changedmetadata.transactions[8].description.logprobsConfidencenullchangedmetadata.transactions[8].description.ocrConfidence0.931changedmetadata.transactions[8].logprobsConfidencenullchangedmetadata.transactions[8].ocrConfidence0.931changedmetadata.transactions[8].reference_number.citations[]changedmetadata.transactions[8].reference_number.logprobsConfidencenullchangedmetadata.transactions[8].transaction_date.logprobsConfidencenullchangedmetadata.transactions[8].transaction_date.ocrConfidence0.99changedmetadata.transactions[8].transaction_type.logprobsConfidencenullchangedmetadata.transactions[8].transaction_type.ocrConfidence0.993changedmetadata.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,"fileId":"file_iryjEgwYKR9xTT8urHNbl","polygon":[{"x":35.712,"y":711.2447999999999},{"x":59.5224,"y":711.216},{"x":59.5296,"y":719.4744},{"x":35.7192…changedmetadata.transactions[9].description.logprobsConfidencenullchangedmetadata.transactions[9].description.ocrConfidence0.988changedmetadata.transactions[9].logprobsConfidencenullchangedmetadata.transactions[9].ocrConfidence0.988changedmetadata.transactions[9].reference_number.citations[]changedmetadata.transactions[9].reference_number.logprobsConfidencenullchangedmetadata.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.account_type"High School Checking"changedvalue.bank_name"JPMorgan Chase Bank, N.A."changedvalue.branch_address"P O Box 182051, Columbus, OH 43218-2051"changedvalue.closing_balance.amount459.25changedvalue.closing_balance.iso_4217_currency_code"USD"changedvalue.opening_balance.amount438.59changedvalue.opening_balance.iso_4217_currency_code"USD"changedvalue.statement_issue_datenullchangedvalue.statement_period_end_date"2021-01-25"changedvalue.statement_period_start_date"2020-12-22"changedvalue.total_credits.amount3098.99changedvalue.total_credits.iso_4217_currency_code"USD"changedvalue.total_debits.amount-3078.33changedvalue.total_debits.iso_4217_currency_code"USD"changedvalue.transactions[{"amount":{"amount":-27.9,"iso_4217_currency_code":"USD"},"description":"Discover E-Payment 6930 Web ID: 2510020270","reference_number":"2510020270","transacti…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 processes bank statements to extract structured financial data: account details (holder name, number, type, bank name), opening/closing balances, statement period dates, and a complete transaction ledger with individual transaction details (date, description, amount, balance after). It converts unstructured PDF statements into machine-readable JSON for reconciliation, reporting, and compliance workflows.
## When to use this
- **Account reconciliation**: Compare extracted transactions against internal records to detect discrepancies.
- **Financial reporting & compliance**: Automate audit trails, regulatory filings, and statement verification.
- **Transaction categorization**: Feed extracted descriptions and amounts into downstream ML classifiers for spending analysis.
- **Fraud detection**: Parse transaction history to identify suspicious patterns, duplicates, or anomalies.
- **Multi-bank consolidation**: Normalize statements from different banks into a single schema for portfolio views.
## Processor pipeline
### Step 1: Parse (agentic OCR + document chunking)
**Processor**: `parse` with `agentic_ocr` mode
**Purpose**: Convert PDF to Markdown, preserving layout and capturing complex text (handwritten notes, signatures, table structures).
**Config choice**: `blockOptions.text.agentic.enabled = true` + `chunkingStrategy.type = "document"` ensures the entire statement stays as one chunk (no page breaks), giving extraction context to infer transaction relationships and balances.
**Why**: Bank statements are dense, tabular documents. Agentic OCR handles scanned/low-quality PDFs; document-level chunking prevents splitting mid-transaction table.
### Step 2: Extract (structured JSON with schema)
**Processor**: `extract` with `extraction_performance` baseProcessor
**Purpose**: Pull account metadata, balances, and all transactions into a strict JSON object with fields: `bank_name`, `account_holder_name`, `account_number`, `account_type`, `opening_balance`, `closing_balance`, `statement_period_start_date`, `statement_period_end_date`, `total_credits`, `total_debits`, and `transactions[]` array.
**Config choice**: Full schema defined below with ISO 4217 currency codes, nullable amounts for edge cases (refunds, reversals), and detailed transaction metadata (reference numbers, balance after each transaction).
**Why**: Bank statements contain precise financial state; extraction schema enforces data type consistency and prevents garbage output. Array nesting captures all 40+ transactions in one call.
## TypeScript implementation
## CLI equivalent
```bash
# Parse the statement to markdown
extend parse statement.pdf \
--block-options '{
"text": {
"agentic": {
"enabled": true
}
}
}' \
--chunking-strategy document
# Extract structured data using the schema JSON
extend extract statement.pdf \
--schema bank-statement-schema.json \
--base-processor extraction_performance
```
**bank-statement-schema.json**:
```json
{
"type": "object",
"properties": {
"bank_name": { "type": ["string", "null"], "description": "Full legal name of the bank" },
"account_holder_name": { "type": ["string", "null"], "description": "Account holder name(s)" },
"account_number": { "type": ["string", "null"], "description": "Account number (may be masked)" },
"account_type": { "type": ["string", "null"], "description": "Checking, Savings, etc." },
"branch_address": { "type": ["string", "null"], "description": "Branch or mailing address" },
"statement_period_start_date": { "type": ["string", "null"], "description": "Start date (yyyy-mm-dd)" },
"statement_period_end_date": { "type": ["string", "null"], "description": "End date (yyyy-mm-dd)" },
"statement_issue_date": { "type": ["string", "null"], "description": "Issue date (yyyy-mm-dd)" },
"opening_balance": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
"closing_balance": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
"total_credits": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
"total_debits": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
"transactions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"transaction_date": { "type": ["string", "null"], "description": "Date (yyyy-mm-dd)" },
"description": { "type": ["string", "null"], "description": "Transaction description" },
"transaction_type": { "type": "string", "enum": ["debit", "credit", "fee", "interest", "other"] },
"amount": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
"balance_after_transaction": { "type": ["object", "null"], "properties": { "amount": { "type": ["number", "null"] }, "iso_4217_currency_code": { "type": ["string", "null"] } } },
"reference_number": { "type": ["string", "null"], "description": "Confirmation or transaction ID" }
}
}
}
}
}
```
## Schema
The extraction schema is a **Zod object** with these top-level fields:
| Field | Type | Nullable | Notes |
|-------|------|----------|-------|
| `bank_name` | string | ✓ | Full legal bank name from statement header |
| `account_holder_name` | string | ✓ | Primary and joint account owners if applicable |
| `account_number` | string | ✓ | Full or masked account number |
| `account_type` | string | ✓ | e.g., "Checking", "Savings", "Money Market" |
| `branch_address` | string | ✓ | Bank branch or statement mailing address |
| `statement_period_start_date` | ISO date | ✗ | First day of statement (yyyy-mm-dd) |
| `statement_period_end_date` | ISO date | ✗ | Last day of statement (yyyy-mm-dd) |
| `statement_issue_date` | ISO date | ✓ | Date statement was printed (if different from period end) |
| `opening_balance` | currency | ✗ | Balance at period start; includes `{ amount, iso_4217_currency_code }` |
| `closing_balance` | currency | ✗ | Balance at period end; same structure |
| `total_credits` | currency | ✗ | Sum of all deposits/credits |
| `total_debits` | currency | ✗ | Sum of all withdrawals/debits (usually negative) |
| `transactions` | array | ✗ | Array of transaction objects (see below) |
### Transaction object fields
| Field | Type | Nullable | Notes |
|-------|------|----------|-------|
| `transaction_date` | ISO date | ✗ | Date transaction posted (yyyy-mm-dd) |
| `description` | string | ✓ | As printed: merchant, check #, transfer detail, etc. |
| `transaction_type` | enum | ✗ | One of: `debit`, `credit`, `fee`, `interest`, `other` |
| `amount` | currency | ✗ | Signed value; negative = debit, positive = credit |
| `balance_after_transaction` | currency | ✗ | Running balance after this transaction |
| `reference_number` | string | ✓ | Confirmation #, check #, reference ID if printed |
---
## Accuracy tips
1. **Description field is highest-import { ExtendClient, extendCurrency, extendDate } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
// Define the Zod schema for bank statement extraction
const BankStatementSchema = z.object({
// Account metadata
bank_name: z.string().nullable().describe(
"Full legal name of the bank that issued this statement, e.g., 'JPMorgan Chase Bank, N.A.'"
),
account_holder_name: z.string().nullable().describe(
"Full name(s) of the account holder(s) as printed on the statement, e.g., 'ANGIE M LOPEZ'"
),
account_number: z.string().nullable().describe(
"Account number, typically masked except last 4 digits, e.g., '000000861006133'"
),
account_type: z.string().nullable().describe(
"Type of account: Checking, Savings, Money Market, etc., e.g., 'High School Checking'"
),
branch_address: z.string().nullable().describe(
"Branch address or mailing address from the statement, e.g., 'P O Box 182051, Columbus, OH 43218-2051'"
),
// Statement period and balances
statement_period_start_date: extendDate().describe(
"Start date of the statement period in ISO 8601 format (yyyy-mm-dd), e.g., '2020-12-22'"
),
statement_period_end_date: extendDate().describe(
"End date (closing date) of the statement period in ISO 8601 format (yyyy-mm-dd), e.g., '2021-01-25'"
),
statement_issue_date: extendDate().nullable().describe(
"Date the statement was issued/printed, if different from period end, in ISO 8601 format"
),
opening_balance: extendCurrency().describe(
"Beginning balance at the start of the statement period"
),
closing_balance: extendCurrency().describe(
"Ending balance at the close of the statement period"
),
total_credits: extendCurrency().describe(
"Sum of all deposits and credits during the period"
),
total_debits: extendCurrency().describe(
"Sum of all withdrawals and debits during the period (as a negative or absolute value)"
),
// Transaction array
transactions: z.array(
z.object({
transaction_date: extendDate().describe(
"Date the transaction posted or was recorded, in ISO 8601 format (yyyy-mm-dd)"
),
description: z.string().nullable().describe(
"Transaction description as printed on the statement, e.g., 'Card Purchase 12/29 Apple.Com/US 800-676-2775 CA'"
),
transaction_type: z.enum(["debit", "credit", "fee", "interest", "other"]).describe(
"Type of transaction: debit (withdrawal/purchase), credit (deposit), fee (service/overdraft fee), interest (earned interest), or other"
),
amount: extendCurrency().describe(
"Transaction amount with sign: negative for debits, positive for credits"
),
balance_after_transaction: extendCurrency().describe(
"Running account balance immediately after this transaction posted"
),
reference_number: z.string().nullable().describe(
"Reference, confirmation, or transaction ID if printed on statement, e.g., 'Web ID: 2510020270'"
),
})
).describe(
"Array of all transactions in the statement period, in chronological order"
),
});
export async function processBankStatement(filePath: string) {
try {
console.log(`Processing bank statement: ${filePath}`);
// Step 1: Read the file and convert to base64 data URL
// (Extend SDK requires a URL; we use data: URL for local files)
const fileBuffer = fs.readFileSync(filePath);
const base64Data = fileBuffer.toString("base64");
const dataUrl = `data:application/pdf;base64,${base64Data}`;
// Step 2: Parse the document (agentic OCR + document-level chunking)
console.log("Step 1: Parsing bank statement with agentic OCR...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
parseConfig: {
blockOptions: {
text: {
agentic: {
enabled: true,
},
},
},
chunkingStrategy: {
type: "document",
},
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
console.log(
`✓ Parsed successfully. Chunks: ${parseRun.output?.chunks?.length || 0}`
);
// Step 3: Extract structured data using the Zod schema
console.log("Step 2: Extracting structured bank statement data...");
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: BankStatementSchema,
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extract failed with status: ${extractRun.status}`);
}
const statement = extractRun.output?.value;
console.log("\n✓ Extraction complete!\n");
console.log("=== BANK STATEMENT SUMMARY ===");
console.log(`Bank: ${statement?.bank_name}`);
console.log(`Account Holder: ${statement?.account_holder_name}`);
console.log(`Account Number: ${statement?.account_number}`);
console.log(`Account Type: ${statement?.account_type}`);
console.log(
`Period: ${statement?.statement_period_start_date} to ${statement?.statement_period_end_date}`
);
console.log(
`Opening Balance: ${statement?.opening_balance?.amount} ${statement?.opening_balance?.iso_4217_currency_code}`
);
console.log(
`Closing Balance: ${statement?.closing_balance?.amount} ${statement?.closing_balance?.iso_4217_currency_code}`
);
console.log(
`Total Credits: ${statement?.total_credits?.amount} ${statement?.total_credits?.iso_4217_currency_code}`
);
console.log(
`Total Debits: ${statement?.total_debits?.amount} ${statement?.total_debits?.iso_4217_currency_code}`
);
console.log(`Transaction Count: ${statement?.transactions?.length || 0}`);
// Display first 5 transactions as examples
if (statement?.transactions && statement.transactions.length > 0) {
console.log("\nFirst 5 Transactions:");
statement.transactions.slice(0, 5).forEach((txn, idx) => {
console.log(
` ${idx + 1}. [${txn.transaction_date}] ${txn.transaction_type.toUpperCase()} ${txn.amount?.amount} ${txn.description}`
);
});
}
// Return the full structured statement
return statement;
} catch (error) {
console.error("Error processing bank statement:", error);
throw error;
}
}
// Allow direct invocation if this file is run as main
if (process.argv[2]) {
processBankStatement(process.argv[2])
.then(() => console.log("\n✓ Done!"))
.catch((err) => {
console.error(err.message || err);
process.exit(1);
});
}import os
import base64
from pathlib import Path
from typing import Any
from extend_ai import Extend
client = Extend(token=os.environ["EXTEND_API_KEY"])
# Define the extraction schema matching the bank statement data model
bank_statement_schema = {
"type": "object",
"properties": {
"bank_name": {
"type": ["string", "null"],
"description": "Name of the bank issuing the statement"
},
"statement_period_start": {
"type": ["string", "null"],
"description": "Start date of the statement period in MM/DD/YYYY format"
},
"statement_period_end": {
"type": ["string", "null"],
"description": "End date of the statement period in MM/DD/YYYY format"
},
"account_number": {
"type": ["string", "null"],
"description": "Customer account number"
},
"account_holder_names": {
"type": ["string", "null"],
"description": "Names of account holders"
},
"account_type": {
"type": ["string", "null"],
"description": "Type of account (e.g., High School Checking)"
},
"beginning_balance": {
"type": ["string", "null"],
"description": "Starting balance in currency format"
},
"ending_balance": {
"type": ["string", "null"],
"description": "Ending balance in currency format"
},
"total_deposits": {
"type": ["string", "null"],
"description": "Total deposits and additions"
},
"total_withdrawals": {
"type": ["string", "null"],
"description": "Combined total of all withdrawals"
},
"total_fees": {
"type": ["string", "null"],
"description": "Total fees charged during period"
},
"transactions": {
"type": "array",
"description": "List of transactions with date, description, and amount"
}
}
}
def process_bank_statement(file_path: str) -> Any:
"""Process a bank statement PDF and extract structured data."""
try:
# Read file and convert to base64 data URL
file_buffer = Path(file_path).read_bytes()
base64_str = base64.b64encode(file_buffer).decode("utf-8")
mime_type = "application/pdf" if file_path.endswith(".pdf") else "application/octet-stream"
data_url = f"data:{mime_type};base64,{base64_str}"
print(f"Processing bank statement: {Path(file_path).name}")
# Step 1: Parse the bank statement with agentic OCR
print("Step 1: Parsing document...")
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"block_options": {
"text": {
"agentic": {
"enabled": True,
},
},
},
"chunking_strategy": {
"type": "document",
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
print(f"✓ Parsed {len(parse_run.output.chunks)} chunks")
markdown = "\n\n".join(chunk.content for chunk in parse_run.output.chunks)
print(f"✓ Markdown preview (first 500 chars):\n{markdown[:500]}...\n")
# Step 2: Extract structured data from the parsed document
print("Step 2: Extracting bank statement fields...")
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": bank_statement_schema,
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed with status: {extract_run.status}")
statement = extract_run.output.value
# Output results
print("\n✓ Extraction successful!\n")
print("=== BANK STATEMENT SUMMARY ===")
print(f"Bank: {statement.get('bank_name') or 'N/A'}")
print(f"Account Holder: {statement.get('account_holder_names') or 'N/A'}")
print(f"Account Number: {statement.get('account_number') or 'N/A'}")
print(f"Account Type: {statement.get('account_type') or 'N/A'}")
print(
f"Statement Period: {statement.get('statement_period_start') or 'N/A'} to {statement.get('statement_period_end') or 'N/A'}"
)
print("\nBalance Summary:")
print(f" Opening: {statement.get('beginning_balance') or '0'}")
print(f" Closing: {statement.get('ending_balance') or '0'}")
print(f" Total Deposits: {statement.get('total_deposits') or '0'}")
print(f" Total Withdrawals: {statement.get('total_withdrawals') or '0'}")
print(f" Total Fees: {statement.get('total_fees') or '0'}")
transactions = statement.get('transactions', [])
print(f"\nTransactions: {len(transactions)} found")
if transactions:
print("\nFirst 5 transactions:")
for idx, txn in enumerate(transactions[:5], 1):
print(f" {idx}. {txn}")
# Return full structured data for downstream processing
print("\n✓ Full statement data available for integration")
return statement
except Exception as error:
print(f"Error processing bank statement: {error}")
raise
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
file_path = sys.argv[1]
try:
process_bank_statement(file_path)
except Exception as err:
print(f"Fatal error: {err}")
sys.exit(1)
else:
print("Usage: python solution.py <path-to-bank-statement.pdf>")// This code uses the Extend REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints with java.net.http.HttpClient (no external dependencies).
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;
import java.util.List;
public class BankStatementProcessor {
private static final String API_BASE_URL = "https://api.extend.ai";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final HttpClient httpClient = HttpClient.newHttpClient();
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("Usage: java BankStatementProcessor <path-to-bank-statement.pdf>");
System.exit(1);
}
String filePath = args[0];
try {
processBankStatement(filePath);
} catch (Exception e) {
System.err.println("Fatal error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
public static void processBankStatement(String filePath) throws Exception {
try {
// Read file and convert to base64 data URL
byte[] fileBuffer = Files.readAllBytes(Paths.get(filePath));
String base64 = Base64.getEncoder().encodeToString(fileBuffer);
String mimeType = filePath.endsWith(".pdf") ? "application/pdf" : "application/octet-stream";
String dataUrl = "data:" + mimeType + ";base64," + base64;
String fileName = Paths.get(filePath).getFileName().toString();
System.out.println("Processing bank statement: " + fileName);
// Step 1: Parse the bank statement with agentic OCR
System.out.println("Step 1: Parsing document...");
Map<String, Object> parseResponse = createAndPollParseRun(dataUrl);
if (!"PROCESSED".equals(parseResponse.get("status"))) {
throw new Exception("Parse failed with status: " + parseResponse.get("status"));
}
Map<String, Object> parseOutput = (Map<String, Object>) parseResponse.get("output");
List<Map<String, Object>> chunks = (List<Map<String, Object>>) parseOutput.get("chunks");
System.out.println("✓ Parsed " + chunks.size() + " chunks");
StringBuilder markdown = new StringBuilder();
for (Map<String, Object> chunk : chunks) {
markdown.append(chunk.get("content")).append("\n\n");
}
String markdownStr = markdown.toString();
System.out.println("✓ Markdown preview (first 500 chars):\n" +
markdownStr.substring(0, Math.min(500, markdownStr.length())) + "...\n");
// Step 2: Extract structured data from the parsed document
System.out.println("Step 2: Extracting bank statement fields...");
Map<String, Object> extractResponse = createAndPollExtractRun(dataUrl);
if (!"PROCESSED".equals(extractResponse.get("status"))) {
throw new Exception("Extraction failed with status: " + extractResponse.get("status"));
}
Map<String, Object> extractOutput = (Map<String, Object>) extractResponse.get("output");
Map<String, Object> statement = (Map<String, Object>) extractOutput.get("value");
// Output results
System.out.println("\n✓ Extraction successful!\n");
System.out.println("=== BANK STATEMENT SUMMARY ===");
System.out.println("Bank: " + (statement.get("bank_name") != null ? statement.get("bank_name") : "N/A"));
System.out.println("Account Holder: " + (statement.get("account_holder_names") != null ? statement.get("account_holder_names") : "N/A"));
System.out.println("Account Number: " + (statement.get("account_number") != null ? statement.get("account_number") : "N/A"));
System.out.println("Account Type: " + (statement.get("account_type") != null ? statement.get("account_type") : "N/A"));
System.out.println("Statement Period: " +
(statement.get("statement_period_start") != null ? statement.get("statement_period_start") : "N/A") +
" to " +
(statement.get("statement_period_end") != null ? statement.get("statement_period_end") : "N/A"));
System.out.println("\nBalance Summary:");
System.out.println(" Opening: " + (statement.get("beginning_balance") != null ? statement.get("beginning_balance") : "0") + " USD");
System.out.println(" Closing: " + (statement.get("ending_balance") != null ? statement.get("ending_balance") : "0") + " USD");
System.out.println(" Total Deposits: " + (statement.get("total_deposits") != null ? statement.get("total_deposits") : "0") + " USD");
System.out.println(" Total Withdrawals: " + (statement.get("total_withdrawals") != null ? statement.get("total_withdrawals") : "0") + " USD");
System.out.println(" Total Fees: " + (statement.get("total_fees") != null ? statement.get("total_fees") : "0") + " USD");
List<Map<String, Object>> transactions = (List<Map<String, Object>>) statement.get("transactions");
int txnCount = transactions != null ? transactions.size() : 0;
System.out.println("\nTransactions: " + txnCount + " found");
if (txnCount > 0) {
System.out.println("\nFirst 5 transactions:");
for (int i = 0; i < Math.min(5, txnCount); i++) {
Map<String, Object> txn = transactions.get(i);
System.out.println(" " + (i + 1) + ". [" + txn.get("date") + "] " +
(txn.get("type") != null ? txn.get("type").toString().toUpperCase() : "UNKNOWN") + " " +
(txn.get("description") != null ? txn.get("description") : ""));
System.out.println(" Amount: " + (txn.get("amount") != null ? txn.get("amount") : "0") +
" USD");
}
}
System.out.println("\n✓ Full statement data available for integration");
} catch (Exception e) {
System.err.println("Error processing bank statement: " + e.getMessage());
throw e;
}
}
private static Map<String, Object> createAndPollParseRun(String dataUrl) throws Exception {
String requestBody = String.format(
"{\"file\":{\"url\":\"%s\"},\"config\":{\"blockOptions\":{\"text\":{\"agentic\":{\"enabled\":true}}},\"chunkingStrategy\":{\"type\":\"document\"}}}",
escapeJson(dataUrl)
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/v1/parse-runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new Exception("Parse API request failed with status " + response.statusCode() + ": " + response.body());
}
return pollForCompletion(parseJsonResponse(response.body()).get("id").toString(), "parse-runs");
}
private static Map<String, Object> createAndPollExtractRun(String dataUrl) throws Exception {
String schemaJson = "{\"type\":\"object\",\"properties\":{\"bank_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the bank issuing the statement\"},\"statement_period_start\":{\"type\":[\"string\",\"null\"],\"description\":\"Start date of the statement period in MM/DD/YYYY format\"},\"statement_period_end\":{\"type\":[\"string\",\"null\"],\"description\":\"End date of the statement period in MM/DD/YYYY format\"},\"account_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Customer account number\"},\"account_holder_names\":{\"type\":[\"string\",\"null\"],\"description\":\"Names of account holders\"},\"account_type\":{\"type\":[\"string\",\"null\"],\"description\":\"Type of account (e.g., High School Checking)\"},\"beginning_balance\":{\"type\":[\"string\",\"null\"],\"description\":\"Starting balance in currency format\"},\"ending_balance\":{\"type\":[\"string\",\"null\"],\"description\":\"Ending balance in currency format\"},\"total_deposits\":{\"type\":[\"string\",\"null\"],\"description\":\"Total deposits and additions\"},\"total_withdrawals\":{\"type\":[\"string\",\"null\"],\"description\":\"Combined total of all withdrawals\"},\"total_fees\":{\"type\":[\"string\",\"null\"],\"description\":\"Total fees charged during period\"},\"transactions\":{\"type\":\"array\",\"description\":\"List of transactions with date, description, and amount\"}}}";
String requestBody = String.format(
"{\"file\":{\"url\":\"%s\"},\"config\":{\"schema\":%s}}",
escapeJson(dataUrl),
schemaJson
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/v1/extract-runs"))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200 && response.statusCode() != 201) {
throw new Exception("Extract API request failed with status " + response.statusCode() + ": " + response.body());
}
return pollForCompletion(parseJsonResponse(response.body()).get("id").toString(), "extract-runs");
}
private static Map<String, Object> pollForCompletion(String runId, String runType) throws Exception {
int maxAttempts = 120;
int attempt = 0;
while (attempt < maxAttempts) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/v1/" + runType + "/" + runId))
.header("Authorization", "Bearer " + API_KEY)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new Exception("Poll request failed with status " + response.statusCode());
}
Map<String, Object> result = parseJsonResponse(response.body());
String status = result.get("status").toString();
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
return result;
}
Thread.sleep(1000);
attempt++;
}
throw new Exception("Polling timeout for " + runType);
}
private static Map<String, Object> parseJsonResponse(String json) {
Map<String, Object> result = new java.util.HashMap<>();
if (json.contains("\"id\"")) {
String id = extractJsonString(json, "id");
result.put("id", id);
}
if (json.contains("\"status\"")) {
String status = extractJsonString(json, "status");
result.put("status", status);
}
if (json.contains("\"output\"")) {
result.put("output", parseJsonObject(json, "output"));
}
return result;
}
private static Map<String, Object> parseJsonObject(String json, String key) {
Map<String, Object> obj = new java.util.HashMap<>();
int startIdx = json.indexOf("\"" + key + "\"");
if (startIdx == -1) return obj;
int colonIdx = json.indexOf(":", startIdx);
int braceIdx = json.indexOf("{", colonIdx);
if (braceIdx == -1) return obj;
int depth = 0;
int endIdx = braceIdx;
for (int i = braceIdx; i < json.length(); i++) {
if (json.charAt(i) == '{') depth++;
if (json.charAt(i) == '}') {
depth--;
if (depth == 0) {
endIdx = i;
break;
}
}
}
String objStr = json.substring(braceIdx, endIdx + 1);
if (objStr.contains("\"chunks\"")) {
obj.put("chunks", parseJsonArray(objStr, "chunks"));
}
if (objStr.contains("\"value\"")) {
obj.put("value", parseJsonValue(objStr, "value"));
}
return obj;
}
private static List<Map<String, Object>> parseJsonArray(String json, String key) {
List<Map<String, Object>> list = new java.util.ArrayList<>();
int startIdx = json.indexOf("\"" + key + "\"");
if (startIdx == -1) return list;
int bracketIdx = json.indexOf("[", startIdx);
if (bracketIdx == -1) return list;
int depth = 0;
int endIdx = bracketIdx;
for (int i = bracketIdx; i < json.length(); i++) {
if (json.charAt(i) == '[') depth++;
if (json.charAt(i) == ']') {
depth--;
if (depth == 0) {
endIdx = i;
break;
}
}
}
String arrayStr = json.substring(bracketIdx + 1, endIdx);
String[] items = arrayStr.split("(?<=}),(?={)");
for (String item : items) {
if (item.trim().isEmpty()) continue;
Map<String, Object> obj = new java.util.HashMap<>();
if (item.contains("\"content\"")) {
obj.put("content", extractJsonString(item, "content"));
}
list.add(obj);
}
return list;
}
private static Map<String, Object> parseJsonValue(String json, String key) {
Map<String, Object> obj = new java.util.HashMap<>();
int startIdx = json.indexOf("\"" + key + "\"");
if (startIdx == -1) return obj;
int colonIdx = json.indexOf(":", startIdx);
int braceIdx = json.indexOf("{", colonIdx);
if (braceIdx == -1) return obj;
int depth = 0;
int endIdx = braceIdx;
for (int i = braceIdx; i < json.length(); i++) {
if (json.charAt(i) == '{') depth++;
if (json.charAt(i) == '}') {
depth--;
if (depth == 0) {
endIdx = i;
break;
}
}
}
String objStr = json.substring(braceIdx, endIdx + 1);
String[] fields = {"bank_name", "account_holder_names", "account_number", "account_type",
"statement_period_start", "statement_period_end", "beginning_balance",
"ending_balance", "total_deposits", "total_withdrawals", "total_fees"};
for (String field : fields) {
if (objStr.contains("\"" + field + "\"")) {
String value = extractJsonString(objStr, field);
obj.put(field, value);
}
}
if (objStr.contains("\"transactions\"")) {
obj.put("transactions", parseJsonArray(objStr, "transactions"));
}
return obj;
}
private static String extractJsonString(String json, String key) {
int startIdx = json.indexOf("\"" + key + "\"");
if (startIdx == -1) return null;
int colonIdx = json.indexOf(":", startIdx);
int quoteIdx = json.indexOf("\"", colonIdx);
if (quoteIdx == -1) return null;
int endIdx = quoteIdx + 1;
while (endIdx < json.length()) {
if (json.charAt(endIdx) == '"' && json.charAt(endIdx - 1) != '\\') {
break;
}
endIdx++;
}
return json.substring(quoteIdx + 1, endIdx);
}
private static String escapeJson(String str) {
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
const extendAPIBase = "https://api.extend.ai"
type Currency struct {
Amount float64 `json:"amount"`
ISO4217CurrencyCode string `json:"iso_4217_currency_code"`
}
type Transaction struct {
TransactionDate string `json:"transaction_date"`
TransactionType *string `json:"transaction_type"`
Description *string `json:"description"`
Amount *Currency `json:"amount"`
ReferenceNumber *string `json:"reference_number"`
BalanceAfterTransaction *Currency `json:"balance_after_transaction"`
}
type BankStatement struct {
BankName *string `json:"bank_name"`
AccountHolderName *string `json:"account_holder_name"`
AccountNumber *string `json:"account_number"`
AccountType *string `json:"account_type"`
StatementPeriodStartDate *string `json:"statement_period_start_date"`
StatementPeriodEndDate *string `json:"statement_period_end_date"`
OpeningBalance *Currency `json:"opening_balance"`
ClosingBalance *Currency `json:"closing_balance"`
TotalDebits *Currency `json:"total_debits"`
TotalCredits *Currency `json:"total_credits"`
Transactions []Transaction `json:"transactions"`
BranchAddress *string `json:"branch_address"`
}
type ParseRunRequest struct {
File FileInput `json:"file"`
Config ParseConfig `json:"config"`
}
type FileInput struct {
URL string `json:"url"`
}
type ParseConfig struct {
BlockOptions BlockOptions `json:"blockOptions"`
ChunkingStrategy ChunkingStrategy `json:"chunkingStrategy"`
}
type BlockOptions struct {
Text TextBlock `json:"text"`
}
type TextBlock struct {
Agentic AgenticConfig `json:"agentic"`
}
type AgenticConfig struct {
Enabled bool `json:"enabled"`
}
type ChunkingStrategy struct {
Type string `json:"type"`
}
type ParseRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output ParseOutput `json:"output"`
}
type ParseOutput struct {
Chunks []Chunk `json:"chunks"`
}
type Chunk struct {
Content string `json:"content"`
}
type ExtractRunRequest struct {
File FileInput `json:"file"`
Config ExtractConfig `json:"config"`
}
type ExtractConfig struct {
Schema map[string]interface{} `json:"schema"`
}
type ExtractRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output ExtractOutput `json:"output"`
}
type ExtractOutput struct {
Value BankStatement `json:"value"`
}
type RunStatusResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output json.RawMessage `json:"output"`
}
func makeRequest(method, endpoint string, body interface{}, token string) ([]byte, error) {
url := extendAPIBase + endpoint
var reqBody io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewBuffer(jsonBody)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(respBody))
}
return respBody, nil
}
func pollParseRun(runID, token string) (*ParseRunResponse, error) {
for {
respBody, err := makeRequest("GET", "/parse-runs/"+runID, nil, token)
if err != nil {
return nil, err
}
var run ParseRunResponse
if err := json.Unmarshal(respBody, &run); err != nil {
return nil, err
}
if run.Status == "PROCESSED" || run.Status == "FAILED" {
return &run, nil
}
time.Sleep(2 * time.Second)
}
}
func pollExtractRun(runID, token string) (*ExtractRunResponse, error) {
for {
respBody, err := makeRequest("GET", "/extract-runs/"+runID, nil, token)
if err != nil {
return nil, err
}
var run ExtractRunResponse
if err := json.Unmarshal(respBody, &run); err != nil {
return nil, err
}
if run.Status == "PROCESSED" || run.Status == "FAILED" {
return &run, nil
}
time.Sleep(2 * time.Second)
}
}
func processBankStatement(filePath string) (*BankStatement, error) {
token := os.Getenv("EXTEND_API_KEY")
if token == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
// Read file and convert to base64 data URL
fileBuffer, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
base64Str := base64.StdEncoding.EncodeToString(fileBuffer)
mimeType := "application/octet-stream"
if strings.HasSuffix(filePath, ".pdf") {
mimeType = "application/pdf"
}
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str)
fmt.Printf("Processing bank statement: %s\n", filepath.Base(filePath))
// Step 1: Parse the bank statement with agentic OCR
fmt.Println("Step 1: Parsing document...")
parseReq := ParseRunRequest{
File: FileInput{URL: dataURL},
Config: ParseConfig{
BlockOptions: BlockOptions{
Text: TextBlock{
Agentic: AgenticConfig{Enabled: true},
},
},
ChunkingStrategy: ChunkingStrategy{Type: "document"},
},
}
respBody, err := makeRequest("POST", "/parse-runs", parseReq, token)
if err != nil {
return nil, err
}
var parseInitResp ParseRunResponse
if err := json.Unmarshal(respBody, &parseInitResp); err != nil {
return nil, err
}
parseRun, err := pollParseRun(parseInitResp.ID, token)
if err != nil {
return nil, err
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
fmt.Printf("✓ Parsed %d chunks\n", len(parseRun.Output.Chunks))
var markdown strings.Builder
for i, chunk := range parseRun.Output.Chunks {
if i > 0 {
markdown.WriteString("\n\n")
}
markdown.WriteString(chunk.Content)
}
markdownStr := markdown.String()
if len(markdownStr) > 500 {
fmt.Printf("✓ Markdown preview (first 500 chars):\n%s...\n\n", markdownStr[:500])
} else {
fmt.Printf("✓ Markdown preview:\n%s\n\n", markdownStr)
}
// Step 2: Extract structured data from the parsed document
fmt.Println("Step 2: Extracting bank statement fields...")
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"bank_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Full legal name of the bank (e.g., 'JPMorgan Chase Bank, N.A.')",
},
"account_holder_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Name(s) of the primary account holder(s)",
},
"account_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Account number, typically last 4 digits visible for security",
},
"account_type": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Type of account (e.g., 'Checking', 'Savings', 'High School Checking')",
},
"statement_period_start_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Start date of the statement period in ISO 8601 format (YYYY-MM-DD)",
},
"statement_period_end_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "End date (closing date) of the statement period in ISO 8601 format (YYYY-MM-DD)",
},
"opening_balance": map[string]interface{}{
"type": "object",
"description": "Account balance at the beginning of the statement period",
},
"closing_balance": map[string]interface{}{
"type": "object",
"description": "Account balance at the end of the statement period",
},
"total_debits": map[string]interface{}{
"type": "object",
"description": "Sum of all debit transactions (withdrawals, purchases, fees)",
},
"total_credits": map[string]interface{}{
"type": "object",
"description": "Sum of all credit transactions (deposits, refunds, transfers in)",
},
"transactions": map[string]interface{}{
"type": "array",
"description": "All transactions in chronological order for the statement period",
},
"branch_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Bank branch address or mailing address from statement header",
},
},
}
extractReq := ExtractRunRequest{
File: FileInput{URL: dataURL},
Config: ExtractConfig{Schema: schema},
}
respBody, err = makeRequest("POST", "/extract-runs", extractReq, token)
if err != nil {
return nil, err
}
var extractInitResp ExtractRunResponse
if err := json.Unmarshal(respBody, &extractInitResp); err != nil {
return nil, err
}
extractRun, err := pollExtractRun(extractInitResp.ID, token)
if err != nil {
return nil, err
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extraction failed with status: %s", extractRun.Status)
}
statement := &extractRun.Output.Value
// Output results
fmt.Println("\n✓ Extraction successful!\n")
fmt.Println("=== BANK STATEMENT SUMMARY ===")
if statement.BankName != nil {
fmt.Printf("Bank: %s\n", *statement.BankName)
} else {
fmt.Println("Bank: N/A")
}
if statement.AccountHolderName != nil {
fmt.Printf("Account Holder: %s\n", *statement.AccountHolderName)
} else {
fmt.Println("Account Holder: N/A")
}
if statement.AccountNumber != nil {
fmt.Printf("Account Number: %s\n", *statement.AccountNumber)
} else {
fmt.Println("Account Number: N/A")
}
if statement.AccountType != nil {
fmt.Printf("Account Type: %s\n", *statement.AccountType)
} else {
fmt.Println("Account Type: N/A")
}
startDate := "N/A"
if statement.StatementPeriodStartDate != nil {
startDate = *statement.StatementPeriodStartDate
}
endDate := "N/A"
if statement.StatementPeriodEndDate != nil {
endDate = *statement.StatementPeriodEndDate
}
fmt.Printf("Statement Period: %s to %s\n", startDate, endDate)
fmt.Println("\nBalance Summary:")
if statement.OpeningBalance != nil {
fmt.Printf(" Opening: %v %s\n", statement.OpeningBalance.Amount, statement.OpeningBalance.ISO4217CurrencyCode)
} else {
fmt.Println(" Opening: 0 USD")
}
if statement.ClosingBalance != nil {
fmt.Printf(" Closing: %v %s\n", statement.ClosingBalance.Amount, statement.ClosingBalance.ISO4217CurrencyCode)
} else {
fmt.Println(" Closing: 0 USD")
}
if statement.TotalDebits != nil {
fmt.Printf(" Total Debits: %v %s\n", statement.TotalDebits.Amount, statement.TotalDebits.ISO4217CurrencyCode)
} else {
fmt.Println(" Total Debits: 0 USD")
}
if statement.TotalCredits != nil {
fmt.Printf(" Total Credits: %v %s\n", statement.TotalCredits.Amount, statement.TotalCredits.ISO4217CurrencyCode)
} else {
fmt.Println(" Total Credits: 0 USD")
}
fmt.Printf("\nTransactions: %d found\n", len(statement.Transactions))
if len(statement.Transactions) > 0 {
fmt.Println("\nFirst 5 transactions:")
limit := 5
if len(statement.Transactions) < 5 {
limit = len(statement.Transactions)
}
for i := 0; i < limit; i++ {
txn := statement.Transactions[i]
txnType := "UNKNOWN"
if txn.TransactionType != nil {
txnType = strings.ToUpper(*txn.TransactionType)
}
desc := "N/A"
if txn.Description != nil {
desc = *txn.Description
}
fmt.Printf(" %d. [%s] %s %s\n", i+1, txn.TransactionDate, txnType, desc)
amount := 0.0
currency := "USD"
if txn.Amount != nil {
amount = txn.Amount.Amount
currency = txn.Amount.ISO4217CurrencyCode
}
balance := 0.0
if txn.BalanceAfterTransaction != nil {
balance = txn.BalanceAfterTransaction.Amount
}
fmt.Printf(" Amount: %v %s, Balance: %v\n", amount, currency, balance)
}
}
fmt.Println("\n✓ Full statement data available for integration")
return statement, nil
}
func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Println("Usage: go run solution.go <path-to-bank-statement.pdf>")
os.Exit(1)
}
filePath := args[0]
_, err := processBankStatement(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %v\n", err)
os.Exit(1)
}
}// Deploy the "Bank Statement" pipeline to YOUR Extend account.
//
// The workflow below is fully self-contained — every EXTRACT/CLASSIFY/SPLIT
// step carries its extractor/classifier/splitter config INLINE, so this is a
// single API call. No processors to create or wire up beforehand.
// Idempotent: the created workflow id is cached in .extend/bank-statement-extractor.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// npx tsx provision.ts
//
// Generated by doc1 (template: bank-statement-extractor).
import fs from "node:fs";
import path from "node:path";
const API = "https://api.extend.ai";
const VERSION = "2026-02-09";
const API_KEY = process.env.EXTEND_API_KEY;
if (!API_KEY) { console.error("Set EXTEND_API_KEY first."); process.exit(1); }
const STATE_DIR = path.join(process.cwd(), ".extend");
const STATE_FILE = path.join(STATE_DIR, "bank-statement-extractor.json");
type State = { workflowId?: string };
const state: State = fs.existsSync(STATE_FILE)
? JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
: {};
function saveState() {
fs.mkdirSync(STATE_DIR, { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
}
async function api(method: string, pathName: string, body?: unknown) {
const res = await fetch(API + pathName, {
method,
headers: {
Authorization: `Bearer ${API_KEY}`,
"x-extend-api-version": VERSION,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} ${pathName} failed (${res.status}): ${JSON.stringify(data).slice(0, 300)}`);
return data;
}
// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
"name": "Bank Statement Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"bank_name": {
"type": [
"string",
"null"
],
"description": "Name of the bank issuing the statement"
},
"statement_period_start": {
"type": [
"string",
"null"
],
"description": "Start date of the statement period in MM/DD/YYYY format"
},
"statement_period_end": {
"type": [
"string",
"null"
],
"description": "End date of the statement period in MM/DD/YYYY format"
},
"account_number": {
"type": [
"string",
"null"
],
"description": "Customer account number"
},
"account_holder_names": {
"type": [
"string",
"null"
],
"description": "Names of account holders"
},
"account_type": {
"type": [
"string",
"null"
],
"description": "Type of account (e.g., High School Checking)"
},
"beginning_balance": {
"type": [
"string",
"null"
],
"description": "Starting balance in currency format"
},
"ending_balance": {
"type": [
"string",
"null"
],
"description": "Ending balance in currency format"
},
"total_deposits": {
"type": [
"string",
"null"
],
"description": "Total deposits and additions"
},
"total_withdrawals": {
"type": [
"string",
"null"
],
"description": "Combined total of all withdrawals"
},
"total_fees": {
"type": [
"string",
"null"
],
"description": "Total fees charged during period"
},
"transactions": {
"type": "array",
"description": "List of transactions with date, description, and amount"
}
}
}
}
}
}
]
};
async function main() {
console.log(`Deploying "${WORKFLOW.name}"…`);
if (state.workflowId) {
console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
} else {
// Reuse an existing workflow with the same name if one exists (e.g. a
// previous run's state file was lost) instead of creating a duplicate.
try {
const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
const existing = items.find((x) => x.name === WORKFLOW.name);
if (existing?.id) {
state.workflowId = existing.id; saveState();
console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
}
} catch { /* lookup is best-effort; fall through to create */ }
if (!state.workflowId) {
const created = await api("POST", "/workflows", WORKFLOW);
const wfId = created.id ?? created.workflow?.id;
if (!wfId) throw new Error("Could not read created workflow id from response");
state.workflowId = wfId; saveState();
console.log(`+ created workflow (${wfId})`);
}
}
// Deploy the current draft as a new version so the workflow is runnable —
// best-effort: some accounts/plans may not require this explicit step.
await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});
console.log("\nDone. Run documents through it with:");
console.log(` POST ${API}/workflow_runs { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}
main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from typing import Any, Optional
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Set EXTEND_API_KEY first.", file=sys.stderr)
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "bank-statement-extractor.json"
state: dict[str, Optional[str]] = {}
if STATE_FILE.exists():
state = json.loads(STATE_FILE.read_text())
def save_state() -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
WORKFLOW = {
"name": "Bank Statement Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [{"step": "parse1"}],
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": True
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [{"step": "extraction2"}],
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"bank_name": {
"type": ["string", "null"],
"description": "Name of the bank issuing the statement"
},
"statement_period_start": {
"type": ["string", "null"],
"description": "Start date of the statement period in MM/DD/YYYY format"
},
"statement_period_end": {
"type": ["string", "null"],
"description": "End date of the statement period in MM/DD/YYYY format"
},
"account_number": {
"type": ["string", "null"],
"description": "Customer account number"
},
"account_holder_names": {
"type": ["string", "null"],
"description": "Names of account holders"
},
"account_type": {
"type": ["string", "null"],
"description": "Type of account (e.g., High School Checking)"
},
"beginning_balance": {
"type": ["string", "null"],
"description": "Starting balance in currency format"
},
"ending_balance": {
"type": ["string", "null"],
"description": "Ending balance in currency format"
},
"total_deposits": {
"type": ["string", "null"],
"description": "Total deposits and additions"
},
"total_withdrawals": {
"type": ["string", "null"],
"description": "Combined total of all withdrawals"
},
"total_fees": {
"type": ["string", "null"],
"description": "Total fees charged during period"
},
"transactions": {
"type": "array",
"description": "List of transactions with date, description, and amount"
}
}
}
}
}
}
]
}
def main() -> None:
client = Extend(token=API_KEY)
print(f"Deploying \"{WORKFLOW['name']}\"…")
if state.get("workflowId"):
workflow_id = state["workflowId"]
print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
else:
# Try to find an existing workflow with the same name
try:
workflows = client.workflows.list(name=WORKFLOW["name"])
items = workflows.data if hasattr(workflows, "data") else (workflows.items if hasattr(workflows, "items") else [])
existing = next((w for w in items if w.name == WORKFLOW["name"]), None)
if existing and existing.id:
state["workflowId"] = existing.id
save_state()
print(f"✓ workflow \"{WORKFLOW['name']}\" found in your account ({existing.id}) — updating steps")
client.workflows.update(id=existing.id, steps=WORKFLOW["steps"])
except Exception:
pass
if not state.get("workflowId"):
created = client.workflows.create(**WORKFLOW)
workflow_id = created.id
if not workflow_id:
raise ValueError("Could not read created workflow id from response")
state["workflowId"] = workflow_id
save_state()
print(f"+ created workflow ({workflow_id})")
# Deploy the current draft as a new version
try:
client.workflows.create_version(id=state["workflowId"])
except Exception:
pass
print("\nDone. Run documents through it with:")
print(f" POST https://api.extend.ai/workflow_runs {{ \"workflow\": {{ \"id\": \"{state['workflowId']}\" }}, \"file\": {{ \"url\": \"https://…\" }} }}")
print("Or open the workflow in the Extend dashboard to review and deploy it.")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)// This code calls Extend's REST API directly using only Java's built-in java.net.http.HttpClient.
// Extend does not publish an official Java SDK; this approach has zero external dependencies.
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class BankStatementProvisioner {
private static final String API = "https://api.extend.ai";
private static final String VERSION = "2026-02-09";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
private static final Path STATE_FILE = STATE_DIR.resolve("bank-statement-extractor.json");
private static final HttpClient HTTP = HttpClient.newHttpClient();
static class State {
String workflowId;
}
private static State state = new State();
public static void main(String[] args) throws Exception {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
loadState();
Map<String, Object> workflow = buildWorkflow();
System.out.println("Deploying \"" + workflow.get("name") + "\"…");
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
Map<String, Object> updateBody = new LinkedHashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + state.workflowId, updateBody);
} else {
try {
String name = (String) workflow.get("name");
String encoded = URLEncoder.encode(name, StandardCharsets.UTF_8);
Map<String, Object> list = api("GET", "/workflows?name=" + encoded, null);
List<?> items = (List<?>) (list.get("data") != null ? list.get("data") : list.get("items"));
if (items == null) items = List.of();
for (Object item : items) {
if (item instanceof Map) {
Map<?, ?> itemMap = (Map<?, ?>) item;
if (name.equals(itemMap.get("name"))) {
String id = (String) itemMap.get("id");
if (id != null) {
state.workflowId = id;
saveState();
System.out.println("✓ workflow \"" + name + "\" found in your account (" + id + ") — updating steps");
Map<String, Object> updateBody = new LinkedHashMap<>();
updateBody.put("steps", workflow.get("steps"));
api("POST", "/workflows/" + id, updateBody);
break;
}
}
}
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (state.workflowId == null || state.workflowId.isEmpty()) {
Map<String, Object> created = api("POST", "/workflows", workflow);
String wfId = (String) created.get("id");
if (wfId == null) {
Map<?, ?> workflowObj = (Map<?, ?>) created.get("workflow");
if (workflowObj != null) {
wfId = (String) workflowObj.get("id");
}
}
if (wfId == null) {
throw new RuntimeException("Could not read created workflow id from response");
}
state.workflowId = wfId;
saveState();
System.out.println("+ created workflow (" + wfId + ")");
}
}
try {
api("POST", "/workflows/" + state.workflowId + "/versions", new LinkedHashMap<>());
} catch (Exception e) {
// best-effort: some accounts/plans may not require this explicit step
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
}
private static void loadState() throws IOException {
if (Files.exists(STATE_FILE)) {
String json = Files.readString(STATE_FILE);
state = parseStateJson(json);
}
}
private static void saveState() throws IOException {
Files.createDirectories(STATE_DIR);
String json = toJson(Map.of("workflowId", state.workflowId != null ? state.workflowId : ""));
Files.writeString(STATE_FILE, json);
}
private static Map<String, Object> api(String method, String pathName, Map<String, Object> body) throws Exception {
String url = API + pathName;
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url))
.method(method, body != null ? HttpRequest.BodyPublishers.ofString(toJson(body)) : HttpRequest.BodyPublishers.noBody())
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
builder.header("Content-Type", "application/json");
}
HttpRequest request = builder.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJson(response.body());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
String errorMsg = toJson(data);
if (errorMsg.length() > 300) {
errorMsg = errorMsg.substring(0, 300);
}
throw new RuntimeException(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
}
return data;
}
private static Map<String, Object> buildWorkflow() {
Map<String, Object> workflow = new LinkedHashMap<>();
workflow.put("name", "Bank Statement Processing Pipeline");
List<Map<String, Object>> steps = List.of(
buildTriggerStep(),
buildParseStep(),
buildExtractionStep()
);
workflow.put("steps", steps);
return workflow;
}
private static Map<String, Object> buildTriggerStep() {
Map<String, Object> step = new LinkedHashMap<>();
step.put("name", "startTrigger1");
step.put("type", "TRIGGER");
step.put("next", List.of(Map.of("step", "parse1")));
return step;
}
private static Map<String, Object> buildParseStep() {
Map<String, Object> step = new LinkedHashMap<>();
step.put("name", "parse1");
step.put("type", "PARSE");
Map<String, Object> config = new LinkedHashMap<>();
Map<String, Object> parseConfig = new LinkedHashMap<>();
Map<String, Object> blockOptions = new LinkedHashMap<>();
Map<String, Object> text = new LinkedHashMap<>();
Map<String, Object> agentic = new LinkedHashMap<>();
agentic.put("enabled", true);
text.put("agentic", agentic);
blockOptions.put("text", text);
parseConfig.put("blockOptions", blockOptions);
parseConfig.put("chunkingStrategy", Map.of("type", "document"));
config.put("parseConfig", parseConfig);
step.put("config", config);
step.put("next", List.of(Map.of("step", "extraction2")));
return step;
}
private static Map<String, Object> buildExtractionStep() {
Map<String, Object> step = new LinkedHashMap<>();
step.put("name", "extraction2");
step.put("type", "EXTRACT");
Map<String, Object> config = new LinkedHashMap<>();
Map<String, Object> extractorConfig = new LinkedHashMap<>();
Map<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("bank_name", Map.of("type", List.of("string", "null"), "description", "Name of the bank issuing the statement"));
properties.put("statement_period_start", Map.of("type", List.of("string", "null"), "description", "Start date of the statement period in MM/DD/YYYY format"));
properties.put("statement_period_end", Map.of("type", List.of("string", "null"), "description", "End date of the statement period in MM/DD/YYYY format"));
properties.put("account_number", Map.of("type", List.of("string", "null"), "description", "Customer account number"));
properties.put("account_holder_names", Map.of("type", List.of("string", "null"), "description", "Names of account holders"));
properties.put("account_type", Map.of("type", List.of("string", "null"), "description", "Type of account (e.g., High School Checking)"));
properties.put("beginning_balance", Map.of("type", List.of("string", "null"), "description", "Starting balance in currency format"));
properties.put("ending_balance", Map.of("type", List.of("string", "null"), "description", "Ending balance in currency format"));
properties.put("total_deposits", Map.of("type", List.of("string", "null"), "description", "Total deposits and additions"));
properties.put("total_withdrawals", Map.of("type", List.of("string", "null"), "description", "Combined total of all withdrawals"));
properties.put("total_fees", Map.of("type", List.of("string", "null"), "description", "Total fees charged during period"));
properties.put("transactions", Map.of("type", "array", "description", "List of transactions with date, description, and amount"));
schema.put("properties", properties);
extractorConfig.put("schema", schema);
config.put("extractorConfig", extractorConfig);
step.put("config", config);
return step;
}
private static String toJson(Object obj) {
if (obj == null) return "null";
if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
if (obj instanceof Number) return obj.toString();
if (obj instanceof Boolean) return obj.toString();
if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(escapeJson(entry.getKey().toString())).append("\":");
sb.append(toJson(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
if (obj instanceof List) {
List<?> list = (List<?>) obj;
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : list) {
if (!first) sb.append(",");
sb.append(toJson(item));
first = false;
}
sb.append("]");
return sb.toString();
}
return "null";
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
}
private static Map<String, Object> parseJson(String json) {
json = json.trim();
if (json.isEmpty() || json.equals("{}")) {
return new LinkedHashMap<>();
}
return parseJsonObject(json, new int[]{0});
}
private static Map<String, Object> parseJsonObject(String json, int[] pos) {
Map<String, Object> map = new LinkedHashMap<>();
skipWhitespace(json, pos);
if (pos[0] >= json.length() || json.charAt(pos[0]) != '{') {
return map;
}
pos[0]++;
skipWhitespace(json, pos);
while (pos[0] < json.length() && json.charAt(pos[0]) != '}') {
skipWhitespace(json, pos);
String key = parseJsonString(json, pos);
skipWhitespace(json, pos);
if (pos[0] < json.length() && json.charAt(pos[0]) == ':') {
pos[0]++;
}
skipWhitespace(json, pos);
Object value = parseJsonValue(json, pos);
map.put(key, value);
skipWhitespace(json, pos);
if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
pos[0]++;
}
skipWhitespace(json, pos);
}
if (pos[0] < json.length() && json.charAt(pos[0]) == '}') {
pos[0]++;
}
return map;
}
private static List<?> parseJsonArray(String json, int[] pos) {
List<Object> list = new java.util.ArrayList<>();
skipWhitespace(json, pos);
if (pos[0] >= json.length() || json.charAt(pos[0]) != '[') {
return list;
}
pos[0]++;
skipWhitespace(json, pos);
while (pos[0] < json.length() && json.charAt(pos[0]) != ']') {
Object value = parseJsonValue(json, pos);
list.add(value);
skipWhitespace(json, pos);
if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
pos[0]++;
}
skipWhitespace(json, pos);
}
if (pos[0] < json.length() && json.charAt(pos[0]) == ']') {
pos[0]++;
}
return list;
}
private static Object parseJsonValue(String json, int[] pos) {
skipWhitespace(json, pos);
if (pos[0] >= json.length()) return null;
char c = json.charAt(pos[0]);
if (c == '{') return parseJsonObject(json, pos);
if (c == '[') return parseJsonArray(json, pos);
if (c == '"') return parseJsonString(json, pos);
if (c == 't' || c == 'f') {
if (json.startsWith("true", pos[0])) {
pos[0] += 4;
return true;
}
if (json.startsWith("false", pos[0])) {
pos[0] += 5;
return false;
}
}
if (c == 'n') {
if (json.startsWith("null", pos[0])) {
pos[0] += 4;
return null;
}
}
if (c == '-' || Character.isDigit(c)) {
int start = pos[0];
if (c == '-') pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
if (pos[0] < json.length() && json.charAt(pos[0]) == '.') {
pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) pos[0]++;
}
String numStr = json.substring(start, pos[0]);
try {
if (numStr.contains(".")) {
return Double.parseDouble(numStr);
} else {
return Long.parseLong(numStr);
}
} catch (NumberFormatException e) {
return numStr;
}
}
return null;
}
private static String parseJsonString(String json, int[] pos) {
skipWhitespace(json, pos);
if (pos[0] >= json.length() || json.charAt(pos[0]) != '"') {
return "";
}
pos[0]++;
StringBuilder sb = new StringBuilder();
while (pos[0] < json.length() && json.charAt(pos[0]) != '"') {
char c = json.charAt(pos[0]);
if (c == '\\' && pos[0] + 1 < json.length()) {
pos[0]++;
char next = json.charAt(pos[0]);
switch (next) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
default: sb.append(next);
}
} else {
sb.append(c);
}
pos[0]++;
}
if (pos[0] < json.length() && json.charAt(pos[0]) == '"') {
pos[0]++;
}
return sb.toString();
}
private static void skipWhitespace(String json, int[] pos) {
while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) {
pos[0]++;
}
}
private static State parseStateJson(String json) {
State s = new State();
Map<String, Object> map = parseJson(json);
Object wfId = map.get("workflowId");
if (wfId instanceof String) {
s.workflowId = (String) wfId;
}
return s;
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It deploys the "Bank Statement" pipeline to your Extend account.
//
// Usage:
// export EXTEND_API_KEY=sk_... (from https://dashboard.extend.ai → API Keys)
// go run provision.go
//
// Generated by doc1 (template: bank-statement-extractor).
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
var (
apiKey string
stateDir string
stateFile string
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
var state State
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
os.Exit(1)
}
stateDir = filepath.Join(wd, ".extend")
stateFile = filepath.Join(stateDir, "bank-statement-extractor.json")
loadState()
}
func loadState() {
data, err := os.ReadFile(stateFile)
if err == nil {
json.Unmarshal(data, &state)
}
}
func saveState() {
os.MkdirAll(stateDir, 0755)
data, _ := json.MarshalIndent(state, "", " ")
os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
var reqBody io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequest(method, API+pathName, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var data map[string]interface{}
json.Unmarshal(respBody, &data)
if resp.StatusCode >= 400 {
respStr := string(respBody)
if len(respStr) > 300 {
respStr = respStr[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, respStr)
}
return data, nil
}
var workflow = map[string]interface{}{
"name": "Bank Statement Processing Pipeline",
"steps": []map[string]interface{}{
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": []map[string]interface{}{
{
"step": "parse1",
},
},
},
{
"name": "parse1",
"type": "PARSE",
"config": map[string]interface{}{
"parseConfig": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]interface{}{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]interface{}{
"type": "document",
},
},
},
"next": []map[string]interface{}{
{
"step": "extraction2",
},
},
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": map[string]interface{}{
"extractorConfig": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"bank_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Name of the bank issuing the statement",
},
"statement_period_start": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Start date of the statement period in MM/DD/YYYY format",
},
"statement_period_end": map[string]interface{}{
"type": []string{"string", "null"},
"description": "End date of the statement period in MM/DD/YYYY format",
},
"account_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Customer account number",
},
"account_holder_names": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Names of account holders",
},
"account_type": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Type of account (e.g., High School Checking)",
},
"beginning_balance": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Starting balance in currency format",
},
"ending_balance": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Ending balance in currency format",
},
"total_deposits": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Total deposits and additions",
},
"total_withdrawals": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Combined total of all withdrawals",
},
"total_fees": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Total fees charged during period",
},
"transactions": map[string]interface{}{
"type": "array",
"description": "List of transactions with date, description, and amount",
},
},
},
},
},
},
},
}
func main() {
workflowName := workflow["name"].(string)
fmt.Printf("Deploying \"%s\"…\n", workflowName)
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{
"steps": workflow["steps"],
})
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
} else {
// Try to find an existing workflow with the same name
listPath := fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflowName))
listResp, err := apiCall("GET", listPath, nil)
if err == nil {
var items []map[string]interface{}
if data, ok := listResp["data"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
} else if data, ok := listResp["items"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
}
for _, item := range items {
if name, ok := item["name"].(string); ok && name == workflowName {
if id, ok := item["id"].(string); ok {
state.WorkflowID = id
saveState()
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflowName, id)
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{
"steps": workflow["steps"],
})
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
break
}
}
}
}
if state.WorkflowID == "" {
created, err := apiCall("POST", "/workflows", workflow)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
var wfID string
if id, ok := created["id"].(string); ok {
wfID = id
} else if wf, ok := created["workflow"].(map[string]interface{}); ok {
if id, ok := wf["id"].(string); ok {
wfID = id
}
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
saveState()
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
// Deploy the current draft as a new version (best-effort)
apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})
fmt.Println("\nDone. Run documents through it with:")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}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.