Financial & BankingParse → Extract

Onboarding Package Extractor

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

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source document802534267-671457213-US-Bank-Statement-BankStatements-pdf-1.pdf

Step-by-step

A bank statement is a periodic financial document issued by a bank that summarizes all transactions, account balances, and account holder information for a given 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 name, account details, balances, and itemized transactions per the extraction schema by using Extend's Parse, Extract primitives.

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

Parse

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

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

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

Step 2

Extract

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

InputOutput of the Parse step
Config
schemacustom schema — 12 fieldschanged
advancedOptions.advancedMultimodalEnabledtruechanged
advancedOptions.reviewAgent.enabledtruechanged
baseProcessor"extraction_performance"
OutputJSON shaped to the extraction schema, with per-field confidence scores and citations grounding each value to its source location

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

Example code

{
  "name": "Bank Statement Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "bank_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the bank issuing the statement"
              },
              "account_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Type of account (e.g., Student Checking)"
              },
              "transactions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "date": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Transaction date"
                    },
                    "amount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Transaction amount"
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Transaction description"
                    },
                    "reference_number": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Reference or confirmation number"
                    }
                  }
                },
                "description": "List of transactions"
              },
              "account_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Bank account number"
              },
              "ending_balance": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Account balance at the end of the statement period"
              },
              "beginning_balance": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Account balance at the start of the statement period"
              },
              "total_withdrawals": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total amount of all withdrawals during the period"
              },
              "account_holder_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the account holder"
              },
              "statement_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "End date of statement period in format MMM DD, YYYY"
              },
              "account_holder_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Mailing address of the account holder"
              },
              "statement_period_start": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Start date of statement period in format MMM DD, YYYY"
              },
              "total_deposits_credits": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total amount of deposits and credits during the period"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Bank Statement Processing — Extend AI Skill

## What this pipeline does

This pipeline converts a bank statement PDF into structured financial data ready for reconciliation, reporting, or accounting system ingestion. It first parses the statement to markdown (capturing layout and tables), then extracts key fields: account metadata (bank name, account number, holder info), statement period, opening/closing balances, totals, and a complete line-by-line transaction list. The output is JSON with full type safety.

## When to use this

- **Personal finance apps** need to auto-import bank statements for reconciliation and budgeting
- **Accounting software** ingests statements for automated bank-to-book matching and variance analysis
- **Loan applications** require statement verification — extract balances and transaction history to verify income/stability
- **Tax preparation** requires historical statements to substantiate income claims and large transfers
- **Financial advisory** workflows need to analyze spending patterns and cash flow from raw statements

## Processor pipeline

| Step | Processor | Purpose | Config | Why |
|------|-----------|---------|--------|-----|
| 1 | **Parse** (`parse_performance`) | Convert PDF to markdown; capture tables and line items | `agentic: true`, `chunkingStrategy: "document"` | Bank statements have dense tables with aligned columns. Agentic mode handles multi-column layouts and extracts transaction tables reliably. Document-level chunking keeps the full statement context in one output. |
| 2 | **Extract** (`extraction_performance`) | Pull 11 fields + transaction array into JSON | Zod schema with `extendCurrency()`, `reviewAgent: true`, `advancedMultimodalEnabled: true` | Currency fields need semantic understanding (e.g., "$1,234.56" → 1234.56). Review agent catches missed transactions or balance mismatches. Multimodal mode handles statements with logos, signatures, or handwritten notes. |

## TypeScript implementation



## CLI equivalent

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

# Step 2: Extract structured fields
extend extract statement.pdf \
  --schema schema.json \
  --advanced-options '{"reviewAgent": {"enabled": true}, "advancedMultimodalEnabled": true}'
```

Where `schema.json` is:

```json
{
  "type": "object",
  "properties": {
    "bank_name": { "type": ["string", "null"], "description": "Name of the bank issuing the statement" },
    "account_number": { "type": ["string", "null"], "description": "Bank account number" },
    "account_type": { "type": ["string", "null"], "description": "Type of account (e.g., Student Checking)" },
    "statement_period_start": { "type": ["string", "null"], "description": "Start date of statement period in format MMM DD, YYYY" },
    "statement_period_end": { "type": ["string", "null"], "description": "End date of statement period in format MMM DD, YYYY" },
    "beginning_balance": { "type": ["number", "null"], "description": "Account balance at the start of the statement period" },
    "ending_balance": { "type": ["number", "null"], "description": "Account balance at the end of the statement period" },
    "total_deposits_credits": { "type": ["number", "null"], "description": "Total amount of deposits and credits during the period" },
    "total_withdrawals": { "type": ["number", "null"], "description": "Total amount of all withdrawals during the period" },
    "account_holder_name": { "type": ["string", "null"], "description": "Name of the account holder" },
    "account_holder_address": { "type": ["string", "null"], "description": "Mailing address of the account holder" },
    "transactions": {
      "type": "array",
      "description": "List of transactions",
      "items": {
        "type": "object",
        "properties": {
          "date": { "type": ["string", "null"], "description": "Transaction date" },
          "description": { "type": ["string", "null"], "description": "Transaction description" },
          "amount": { "type": ["number", "null"], "description": "Transaction amount" },
          "reference_number": { "type": ["string", "null"], "description": "Reference or confirmation number" }
        }
      }
    }
  }
}
```

Or run a pre-saved workflow in one command:

```bash
extend run workflow_abc123 --file statement.pdf
```

## Schema

```typescript
{
  bank_name: string | null
    // Why: Banks operate under many legal names (holding company vs. operating entity).
    // Accuracy lever: Mark as required (non-nullable) if your system integrates with
    // a known bank list; leave nullable if you accept statements from any bank.

  account_number: string | null
    // Why: Core identifier for reconciliation; often masked or partial on statements.
    // Accuracy lever: If statement shows "****1234", document clearly that extraction
    // captures visible digits only; coordinate with your bank's API for full number.

  account_type: string | null
    // Why: Distinguishes checking, savings, money market, etc.; affects reconciliation rules.
    // Accuracy lever: Provide examples in description ("Checking", "Savings", "Student Checking").

  statement_period_start: string | null
    // Why: Critical for matching deposits/withdrawals to GL periods and fiscal calendars.
    // Format: MMM DD, YYYY (e.g., "Jan 01, 2024")
    // Accuracy lever: Enforce format in post-processing; reject if period is > 45 days.

  statement_period_end: string | null
    // Why: Defines the statement window; mismatches here cause reconciliation failures.
    // Format: MMM DD, YYYY (e.g., "Jan 31, 2024")
    // Accuracy lever: Validate end >= start and end <= today.

  beginning_balance: number | null
    // Why: Starting point for all reconciliation; must be validated against prior period's closing.
    // Accuracy lever: If null, query prior statement; if mismatch > $0.01, flag for review.

  ending_balance: number | null
    // Why: Expected balance after all transactions; validates completeness.
    // Accuracy lever: Check: ending_balance == beginning_balance + deposits - withdrawals.
    // If mismatch > $0.01, trigger human review.

  total_deposits_credits: number | null
    // Why: Summary line for reconciliation speedup; sum of all deposits and interest.
    // Accuracy lever: Validate against sum(transactions where amount > 0).

  total_withdrawals: number | null
    // Why: Summary line for reconciliation; sum of all debits, transfers, fees.
    // Accuracy lever: Validate against sum(transactions where amount < 0).

  account_holder_name: string | null
    // Why: Verify statement ownership; required for KYC/AML and fraud checks.
    // Accuracy lever: Compare against known customer name; flag if mismatch.

  account_holder_address: string | null
    // Why: Verify mailing address; used for statement delivery and identity confirmation.
    // Accuracy lever: Normalize address and compare against known record; flag if city or state differs.

  transactions: array of {
    date: string | null
      // Format: YYYY-MM-DD (ISO 8601) or as printed (e.g., "01/31/2024")
      // Why: Ties transaction to clearing date; critical for matching GL postings.
      // Accuracy lever: Parse all date formats banks use; validate date is within statement period.

    description: string | null
      // Why: Identifies payee or transaction type (e.g., "ACH DEPOSIT PAYROLL", "WIRE OUT").
      // Accuracy lever: Normalize descriptions (trim whitespace, capitalize); use for merchant categorization.

    amount: number | null
      // Why: Transaction value in account currency.
      // Accuracy lever: Store as positive; encode direction in transaction type field (debit vs. credit).
      // Validate no amount > account balance or < -account balance.

    reference_number: string | null
      // Why: Bank's unique transaction ID; used for matching bank details to GL and for dispute resolution.
      // Accuracy lever: Preserve exactly as printed; required field for ACH and wire matches.
  }
}
```

## Accuracy tips

1. **Enforce date format validation** — Bank statements use many formats (MM/DD/YYYY, DD/MM/YYYY, "Jan 31"). Parse all variants; store as ISO 8601. If extraction is ambiguous, flag for review.

2. **Validate balance equation** — Check: `ending_balance == beginning_balance + sum(deposits) - sum(withdrawals)`. If variance > $0.01, the statement may be incomplete or a transaction was missed. Trigger automatic re-extraction or human review.

3. **Match transaction count** — Count line items extracted vs. statement's printed total ("123 transactions"). If mismatch > 5%, re-extract with `advancedMultimodalEnabled: true` to handle scanned or low-quality originals.

4. **Normalize transaction descriptions** — Banks use inconsistent naming (e.g., "POS DEBIT", "POS PURCHASE", "DEBIT CARD PURCHASE" are all point-of-sale). Normalize to a canonical vocabulary pre-categorization.

5. **Preserve reference numbers exactly** — Wire confirmations, ACH trace numbers, and check numbers are used for matching and disputes. Do not truncate or reformat; store as-is.

6. **Use review agent for high-value statements** — Enable `reviewAgent: true` for statements > $100k total activity or > 500 transactions. It catches missing line items and validates that totals reconcile.

7. **Handle multi-page statements** — Use `chunkingStrategy: "document"` (not page-level) so extraction sees all transactions in one context. For statements > 20 pages, consider async extraction (`createAndPoll` is fine; it handles large files).

8. **Validate account holder identity** — Extract `account_holder_name` and `account_holder_address` on every statement; compare against known customer record. Flag if name or city/state differs.

9. **Test with scanned/faxed originals** — Enable `advancedMultimodalEnabled: true` for statements from customers with older scanning workflows. This handles rotated text, logos, watermarks, and faded ink.

10. **Post-process currency symbols** — Ensure `beginning_balance`,
import fs from "fs";
import { ExtendClient, extendCurrency } from "extend-ai";
import { z } from "zod";

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

// Define the Zod schema for bank statement extraction
const BankStatementSchema = z.object({
  bank_name: z.string().nullable().describe("Name of the bank issuing the statement"),
  account_number: z.string().nullable().describe("Bank account number"),
  account_type: z.string().nullable().describe("Type of account (e.g., Student Checking)"),
  statement_period_start: z.string().nullable().describe("Start date of statement period in format MMM DD, YYYY"),
  statement_period_end: z.string().nullable().describe("End date of statement period in format MMM DD, YYYY"),
  beginning_balance: z.number().nullable().describe("Account balance at the start of the statement period"),
  ending_balance: z.number().nullable().describe("Account balance at the end of the statement period"),
  total_deposits_credits: z.number().nullable().describe("Total amount of deposits and credits during the period"),
  total_withdrawals: z.number().nullable().describe("Total amount of all withdrawals during the period"),
  account_holder_name: z.string().nullable().describe("Name of the account holder"),
  account_holder_address: z.string().nullable().describe("Mailing address of the account holder"),
  transactions: z.array(
    z.object({
      date: z.string().nullable().describe("Transaction date"),
      description: z.string().nullable().describe("Transaction description"),
      amount: z.number().nullable().describe("Transaction amount"),
      reference_number: z.string().nullable().describe("Reference or confirmation number"),
    })
  ).describe("List of transactions"),
});

type BankStatement = z.infer<typeof BankStatementSchema>;

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

  // Step 1: Read file and convert to data URL for SDK
  const fileBuffer = fs.readFileSync(filePath);
  const dataUrl = `data:application/pdf;base64,${fileBuffer.toString("base64")}`;

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

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

  // Extract markdown content for optional logging/debugging
  const markdownContent = parseRun.output.chunks
    .map((chunk) => chunk.content)
    .join("\n\n");
  console.log(`Parsed ${markdownContent.length} characters of markdown`);

  // Step 3: Extract structured fields from the parsed content
  console.log("Step 2: Extracting structured bank statement data...");
  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: BankStatementSchema,
      advancedOptions: {
        reviewAgent: {
          enabled: true, // Validates transaction totals and balance consistency
        },
        advancedMultimodalEnabled: true, // Handles logos, signatures, scanned documents
      },
    },
  });

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

  const statement: BankStatement = extractRun.output.value;

  // Step 4: Validate extracted data
  console.log("\n=== Extracted Bank Statement ===");
  console.log(`Bank: ${statement.bank_name}`);
  console.log(`Account: ${statement.account_number} (${statement.account_type})`);
  console.log(`Holder: ${statement.account_holder_name}`);
  console.log(`Period: ${statement.statement_period_start} to ${statement.statement_period_end}`);
  console.log(`Beginning Balance: $${statement.beginning_balance?.toFixed(2) ?? "null"}`);
  console.log(`Ending Balance: $${statement.ending_balance?.toFixed(2) ?? "null"}`);
  console.log(`Total Deposits: $${statement.total_deposits_credits?.toFixed(2) ?? "null"}`);
  console.log(`Total Withdrawals: $${statement.total_withdrawals?.toFixed(2) ?? "null"}`);
  console.log(`Transaction Count: ${statement.transactions?.length ?? 0}`);

  // Log first 3 transactions for verification
  if (statement.transactions && statement.transactions.length > 0) {
    console.log("\nFirst 3 transactions:");
    statement.transactions.slice(0, 3).forEach((txn, idx) => {
      console.log(
        `  ${idx + 1}. [${txn.date}] ${txn.description} | $${txn.amount?.toFixed(2) ?? "null"} | Ref: ${txn.reference_number}`
      );
    });
  }

  return statement;
}

// Auto-invoke if called directly
const filePath = process.argv[2];
if (filePath) {
  processBankStatement(filePath)
    .then((result) => {
      console.log("\nFinal output:");
      console.log(JSON.stringify(result, null, 2));
    })
    .catch((err) => {
      console.error("Error processing bank statement:", err);
      process.exit(1);
    });
}
import os
import sys
import json
import base64
from typing import Optional
from extend_ai import Extend


# Define the extraction schema as a dictionary
BANK_STATEMENT_SCHEMA = {
    "type": "object",
    "properties": {
        "bank_name": {
            "type": ["string", "null"],
            "description": "Name of the bank issuing the statement",
        },
        "account_number": {
            "type": ["string", "null"],
            "description": "Bank account number",
        },
        "account_type": {
            "type": ["string", "null"],
            "description": "Type of account (e.g., Student Checking)",
        },
        "statement_period_start": {
            "type": ["string", "null"],
            "description": "Start date of statement period in format MMM DD, YYYY",
        },
        "statement_period_end": {
            "type": ["string", "null"],
            "description": "End date of statement period in format MMM DD, YYYY",
        },
        "beginning_balance": {
            "type": ["number", "null"],
            "description": "Account balance at the start of the statement period",
        },
        "ending_balance": {
            "type": ["number", "null"],
            "description": "Account balance at the end of the statement period",
        },
        "total_deposits_credits": {
            "type": ["number", "null"],
            "description": "Total amount of deposits and credits during the period",
        },
        "total_withdrawals": {
            "type": ["number", "null"],
            "description": "Total amount of all withdrawals during the period",
        },
        "account_holder_name": {
            "type": ["string", "null"],
            "description": "Name of the account holder",
        },
        "account_holder_address": {
            "type": ["string", "null"],
            "description": "Mailing address of the account holder",
        },
        "transactions": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "date": {
                        "type": ["string", "null"],
                        "description": "Transaction date",
                    },
                    "description": {
                        "type": ["string", "null"],
                        "description": "Transaction description",
                    },
                    "amount": {
                        "type": ["number", "null"],
                        "description": "Transaction amount",
                    },
                    "reference_number": {
                        "type": ["string", "null"],
                        "description": "Reference or confirmation number",
                    },
                },
            },
            "description": "List of transactions",
        },
    },
}


async def process_bank_statement(file_path: str) -> dict:
    """Process a bank statement file and extract structured data."""
    print(f"Processing bank statement: {file_path}")

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

    # Step 1: Read file and convert to data URL
    with open(file_path, "rb") as f:
        file_buffer = f.read()
    data_url = f"data:application/pdf;base64,{base64.b64encode(file_buffer).decode('utf-8')}"

    # Step 2: Parse the bank statement to markdown
    print("Step 1: Parsing bank statement...")
    parse_run = await 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}")

    # Extract markdown content for optional logging/debugging
    markdown_content = "\n\n".join(
        [chunk.content for chunk in parse_run.output.chunks]
    )
    print(f"Parsed {len(markdown_content)} characters of markdown")

    # Step 3: Extract structured fields from the parsed content
    print("Step 2: Extracting structured bank statement data...")
    extract_run = await client.extract_runs.create_and_poll(
        file={"url": data_url},
        config={
            "schema": BANK_STATEMENT_SCHEMA,
            "advanced_options": {
                "review_agent": {
                    "enabled": True,  # Validates transaction totals and balance consistency
                },
                "advanced_multimodal_enabled": True,  # Handles logos, signatures, scanned documents
            },
        },
    )

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

    statement = extract_run.output.value

    # Step 4: Validate extracted data
    print("\n=== Extracted Bank Statement ===")
    print(f"Bank: {statement.get('bank_name')}")
    print(
        f"Account: {statement.get('account_number')} ({statement.get('account_type')})"
    )
    print(f"Holder: {statement.get('account_holder_name')}")
    print(
        f"Period: {statement.get('statement_period_start')} to {statement.get('statement_period_end')}"
    )

    beginning_balance = statement.get("beginning_balance")
    print(
        f"Beginning Balance: ${beginning_balance:.2f if beginning_balance is not None else 'null'}"
    )

    ending_balance = statement.get("ending_balance")
    print(f"Ending Balance: ${ending_balance:.2f if ending_balance is not None else 'null'}")

    total_deposits = statement.get("total_deposits_credits")
    print(f"Total Deposits: ${total_deposits:.2f if total_deposits is not None else 'null'}")

    total_withdrawals = statement.get("total_withdrawals")
    print(f"Total Withdrawals: ${total_withdrawals:.2f if total_withdrawals is not None else 'null'}")

    transactions = statement.get("transactions", [])
    print(f"Transaction Count: {len(transactions) if transactions else 0}")

    # Log first 3 transactions for verification
    if transactions and len(transactions) > 0:
        print("\nFirst 3 transactions:")
        for idx, txn in enumerate(transactions[:3]):
            txn_date = txn.get("date")
            txn_desc = txn.get("description")
            txn_amount = txn.get("amount")
            txn_ref = txn.get("reference_number")
            amount_str = f"{txn_amount:.2f}" if txn_amount is not None else "null"
            print(
                f"  {idx + 1}. [{txn_date}] {txn_desc} | ${amount_str} | Ref: {txn_ref}"
            )

    return statement


async def main():
    """Main entry point."""
    if len(sys.argv) < 2:
        print("Usage: python solution.py <file_path>")
        sys.exit(1)

    file_path = sys.argv[1]

    try:
        result = await process_bank_statement(file_path)
        print("\nFinal output:")
        print(json.dumps(result, indent=2))
    except Exception as err:
        print(f"Error processing bank statement: {err}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())
// NOTE: Extend does not publish an official Java SDK.
// This code calls the REST API directly using java.net.http.HttpClient.
// No external dependencies are required—only Java's built-in libraries.

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.List;
import java.util.Map;
import java.util.Objects;

public class BankStatementProcessor {
    private static final String API_BASE = "https://api.extend.ai";
    private static final String API_KEY = System.getenv("EXTEND_API_KEY");
    private final HttpClient httpClient = HttpClient.newHttpClient();

    public static class Transaction {
        public String date;
        public String description;
        public Double amount;
        public String reference_number;
    }

    public static class BankStatement {
        public String bank_name;
        public String account_number;
        public String account_type;
        public String statement_period_start;
        public String statement_period_end;
        public Double beginning_balance;
        public Double ending_balance;
        public Double total_deposits_credits;
        public Double total_withdrawals;
        public String account_holder_name;
        public String account_holder_address;
        public List<Transaction> transactions;
    }

    private String makeRequest(String method, String path, String jsonBody) throws IOException, InterruptedException {
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
                .uri(URI.create(API_BASE + path))
                .header("Authorization", "Bearer " + API_KEY)
                .header("Content-Type", "application/json");

        if ("POST".equals(method)) {
            requestBuilder.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
        } else if ("GET".equals(method)) {
            requestBuilder.GET();
        }

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

        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new IOException("API request failed with status " + response.statusCode() + ": " + response.body());
        }
        return response.body();
    }

    private String pollParseRun(String runId) throws IOException, InterruptedException {
        while (true) {
            String response = makeRequest("GET", "/v1/parse-runs/" + runId, null);
            if (response.contains("\"status\":\"PROCESSED\"")) {
                return response;
            }
            if (response.contains("\"status\":\"FAILED\"")) {
                throw new IOException("Parse run failed with status: FAILED");
            }
            Thread.sleep(2000);
        }
    }

    private String pollExtractRun(String runId) throws IOException, InterruptedException {
        while (true) {
            String response = makeRequest("GET", "/v1/extract-runs/" + runId, null);
            if (response.contains("\"status\":\"PROCESSED\"")) {
                return response;
            }
            if (response.contains("\"status\":\"FAILED\"")) {
                throw new IOException("Extract run failed with status: FAILED");
            }
            Thread.sleep(2000);
        }
    }

    public BankStatement processBankStatement(String filePath) throws IOException, InterruptedException {
        System.out.println("Processing bank statement: " + filePath);

        // Step 1: Read file and convert to data URL
        byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
        String base64Data = Base64.getEncoder().encodeToString(fileBytes);
        String dataUrl = "data:application/pdf;base64," + base64Data;

        // Step 2: Parse the bank statement to markdown
        System.out.println("Step 1: Parsing bank statement...");
        String parseRequestBody = String.format(
                "{\"file\":{\"url\":\"%s\"},\"config\":{\"blockOptions\":{\"text\":{\"agentic\":{\"enabled\":true}}},\"chunkingStrategy\":{\"type\":\"document\"}}}",
                dataUrl.replace("\"", "\\\""));

        String parseResponse = makeRequest("POST", "/v1/parse-runs", parseRequestBody);
        String parseRunId = extractField(parseResponse, "id");

        String parseRunStatus = pollParseRun(parseRunId);
        if (!parseRunStatus.contains("\"status\":\"PROCESSED\"")) {
            throw new IOException("Parse failed with status: " + extractField(parseRunStatus, "status"));
        }

        System.out.println("Parse completed successfully");

        // Step 3: Extract structured fields from the parsed content
        System.out.println("Step 2: Extracting structured bank statement data...");
        String extractRequestBody = String.format(
                "{\"file\":{\"url\":\"%s\"},\"config\":{\"schema\":%s,\"advancedOptions\":{\"reviewAgent\":{\"enabled\":true},\"advancedMultimodalEnabled\":true}}}",
                dataUrl.replace("\"", "\\\""), getBankStatementSchema().replace("\"", "\\\""));

        String extractResponse = makeRequest("POST", "/v1/extract-runs", extractRequestBody);
        String extractRunId = extractField(extractResponse, "id");

        String extractRunStatus = pollExtractRun(extractRunId);
        if (!extractRunStatus.contains("\"status\":\"PROCESSED\"")) {
            throw new IOException("Extraction failed with status: " + extractField(extractRunStatus, "status"));
        }

        BankStatement statement = parseExtractOutput(extractRunStatus);

        // Step 4: Display extracted data
        System.out.println("\n=== Extracted Bank Statement ===");
        System.out.println("Bank: " + statement.bank_name);
        System.out.println("Account: " + statement.account_number + " (" + statement.account_type + ")");
        System.out.println("Holder: " + statement.account_holder_name);
        System.out.println("Period: " + statement.statement_period_start + " to " + statement.statement_period_end);
        System.out.println("Beginning Balance: $" + (statement.beginning_balance != null ? String.format("%.2f", statement.beginning_balance) : "null"));
        System.out.println("Ending Balance: $" + (statement.ending_balance != null ? String.format("%.2f", statement.ending_balance) : "null"));
        System.out.println("Total Deposits: $" + (statement.total_deposits_credits != null ? String.format("%.2f", statement.total_deposits_credits) : "null"));
        System.out.println("Total Withdrawals: $" + (statement.total_withdrawals != null ? String.format("%.2f", statement.total_withdrawals) : "null"));
        System.out.println("Transaction Count: " + (statement.transactions != null ? statement.transactions.size() : 0));

        if (statement.transactions != null && !statement.transactions.isEmpty()) {
            System.out.println("\nFirst 3 transactions:");
            for (int i = 0; i < Math.min(3, statement.transactions.size()); i++) {
                Transaction txn = statement.transactions.get(i);
                System.out.println(String.format("  %d. [%s] %s | $%s | Ref: %s",
                        i + 1,
                        txn.date,
                        txn.description,
                        txn.amount != null ? String.format("%.2f", txn.amount) : "null",
                        txn.reference_number));
            }
        }

        return statement;
    }

    private String extractField(String json, String fieldName) {
        String pattern = "\"" + fieldName + "\":\"([^\"]*)\"";
        java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
        java.util.regex.Matcher m = p.matcher(json);
        return m.find() ? m.group(1) : null;
    }

    private BankStatement parseExtractOutput(String json) {
        BankStatement statement = new BankStatement();
        statement.bank_name = extractField(json, "bank_name");
        statement.account_number = extractField(json, "account_number");
        statement.account_type = extractField(json, "account_type");
        statement.statement_period_start = extractField(json, "statement_period_start");
        statement.statement_period_end = extractField(json, "statement_period_end");
        statement.account_holder_name = extractField(json, "account_holder_name");
        statement.account_holder_address = extractField(json, "account_holder_address");

        String beginningBalanceStr = extractNumericField(json, "beginning_balance");
        statement.beginning_balance = beginningBalanceStr != null ? Double.parseDouble(beginningBalanceStr) : null;

        String endingBalanceStr = extractNumericField(json, "ending_balance");
        statement.ending_balance = endingBalanceStr != null ? Double.parseDouble(endingBalanceStr) : null;

        String depositsStr = extractNumericField(json, "total_deposits_credits");
        statement.total_deposits_credits = depositsStr != null ? Double.parseDouble(depositsStr) : null;

        String withdrawalsStr = extractNumericField(json, "total_withdrawals");
        statement.total_withdrawals = withdrawalsStr != null ? Double.parseDouble(withdrawalsStr) : null;

        statement.transactions = new java.util.ArrayList<>();
        return statement;
    }

    private String extractNumericField(String json, String fieldName) {
        String pattern = "\"" + fieldName + "\":([0-9.]+)";
        java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
        java.util.regex.Matcher m = p.matcher(json);
        return m.find() ? m.group(1) : null;
    }

    private String getBankStatementSchema() {
        return "{\"type\":\"object\",\"properties\":{\"bank_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the bank issuing the statement\"},\"account_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Bank account number\"},\"account_type\":{\"type\":[\"string\",\"null\"],\"description\":\"Type of account\"},\"statement_period_start\":{\"type\":[\"string\",\"null\"],\"description\":\"Start date of statement period\"},\"statement_period_end\":{\"type\":[\"string\",\"null\"],\"description\":\"End date of statement period\"},\"beginning_balance\":{\"type\":[\"number\",\"null\"],\"description\":\"Account balance at start\"},\"ending_balance\":{\"type\":[\"number\",\"null\"],\"description\":\"Account balance at end\"},\"total_deposits_credits\":{\"type\":[\"number\",\"null\"],\"description\":\"Total deposits and credits\"},\"total_withdrawals\":{\"type\":[\"number\",\"null\"],\"description\":\"Total withdrawals\"},\"account_holder_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of account holder\"},\"account_holder_address\":{\"type\":[\"string\",\"null\"],\"description\":\"Mailing address\"},\"transactions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"date\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"amount\":{\"type\":[\"number\",\"null\"]},\"reference_number\":{\"type\":[\"string\",\"null\"]}}},\"description\":\"List of transactions\"}}}";
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        if (args.length == 0) {
            System.err.println("Usage: java BankStatementProcessor <filePath>");
            System.exit(1);
        }

        String filePath = args[0];
        BankStatementProcessor processor = new BankStatementProcessor();

        try {
            BankStatement result = processor.processBankStatement(filePath);
            System.out.println("\nFinal output:");
            System.out.println(toJson(result));
        } catch (Exception e) {
            System.err.println("Error processing bank statement: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }

    private static String toJson(Object obj) {
        return obj.toString();
    }
}
// This code calls Extend's REST API directly using only Go's standard library.
// Extend does not publish an official Go SDK; the TypeScript SDK is a thin wrapper
// over these same REST endpoints. We use net/http and encoding/json for zero dependencies.
package main

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

const (
	apiBaseURL = "https://api.extend.ai"
)

// BankStatement represents the extracted bank statement data
type BankStatement struct {
	BankName              *string       `json:"bank_name"`
	AccountNumber         *string       `json:"account_number"`
	AccountType           *string       `json:"account_type"`
	StatementPeriodStart  *string       `json:"statement_period_start"`
	StatementPeriodEnd    *string       `json:"statement_period_end"`
	BeginningBalance      *float64      `json:"beginning_balance"`
	EndingBalance         *float64      `json:"ending_balance"`
	TotalDepositsCredits  *float64      `json:"total_deposits_credits"`
	TotalWithdrawals      *float64      `json:"total_withdrawals"`
	AccountHolderName     *string       `json:"account_holder_name"`
	AccountHolderAddress  *string       `json:"account_holder_address"`
	Transactions          []Transaction `json:"transactions"`
}

// Transaction represents a single bank transaction
type Transaction struct {
	Date            *string  `json:"date"`
	Description     *string  `json:"description"`
	Amount          *float64 `json:"amount"`
	ReferenceNumber *string  `json:"reference_number"`
}

// parseRunResponse represents the response from a parse run
type parseRunResponse struct {
	Status string `json:"status"`
	Output struct {
		Chunks []struct {
			Content string `json:"content"`
		} `json:"chunks"`
	} `json:"output"`
}

// extractRunResponse represents the response from an extract run
type extractRunResponse struct {
	Status string `json:"status"`
	Output struct {
		Value BankStatement `json:"value"`
	} `json:"output"`
}

// pollResponse is the generic polling response
type pollResponse struct {
	Status string          `json:"status"`
	Output json.RawMessage `json:"output"`
}

func processBankStatement(filePath string) (*BankStatement, error) {
	fmt.Printf("Processing bank statement: %s\n", filePath)

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

	// Step 1: Read file and convert to data URL
	fileBuffer, err := ioutil.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read file: %w", err)
	}
	dataURL := fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(fileBuffer))

	// Step 2: Parse the bank statement to markdown
	fmt.Println("Step 1: Parsing bank statement...")
	parseRunID, err := createParseRun(apiKey, dataURL)
	if err != nil {
		return nil, fmt.Errorf("failed to create parse run: %w", err)
	}

	parseResult, err := pollParseRun(apiKey, parseRunID)
	if err != nil {
		return nil, fmt.Errorf("failed to poll parse run: %w", err)
	}

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

	// Extract markdown content for logging
	markdownLength := 0
	for _, chunk := range parseResult.Output.Chunks {
		markdownLength += len(chunk.Content)
	}
	fmt.Printf("Parsed %d characters of markdown\n", markdownLength)

	// Step 3: Extract structured fields from the parsed content
	fmt.Println("Step 2: Extracting structured bank statement data...")
	extractRunID, err := createExtractRun(apiKey, dataURL)
	if err != nil {
		return nil, fmt.Errorf("failed to create extract run: %w", err)
	}

	extractResult, err := pollExtractRun(apiKey, extractRunID)
	if err != nil {
		return nil, fmt.Errorf("failed to poll extract run: %w", err)
	}

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

	statement := &extractResult.Output.Value

	// Step 4: Validate and log extracted data
	fmt.Println("\n=== Extracted Bank Statement ===")
	fmt.Printf("Bank: %v\n", statement.BankName)
	accountTypeStr := "unknown"
	if statement.AccountType != nil {
		accountTypeStr = *statement.AccountType
	}
	fmt.Printf("Account: %v (%s)\n", statement.AccountNumber, accountTypeStr)
	fmt.Printf("Holder: %v\n", statement.AccountHolderName)
	fmt.Printf("Period: %v to %v\n", statement.StatementPeriodStart, statement.StatementPeriodEnd)
	if statement.BeginningBalance != nil {
		fmt.Printf("Beginning Balance: $%.2f\n", *statement.BeginningBalance)
	} else {
		fmt.Println("Beginning Balance: null")
	}
	if statement.EndingBalance != nil {
		fmt.Printf("Ending Balance: $%.2f\n", *statement.EndingBalance)
	} else {
		fmt.Println("Ending Balance: null")
	}
	if statement.TotalDepositsCredits != nil {
		fmt.Printf("Total Deposits: $%.2f\n", *statement.TotalDepositsCredits)
	} else {
		fmt.Println("Total Deposits: null")
	}
	if statement.TotalWithdrawals != nil {
		fmt.Printf("Total Withdrawals: $%.2f\n", *statement.TotalWithdrawals)
	} else {
		fmt.Println("Total Withdrawals: null")
	}
	fmt.Printf("Transaction Count: %d\n", len(statement.Transactions))

	// Log first 3 transactions for verification
	if len(statement.Transactions) > 0 {
		fmt.Println("\nFirst 3 transactions:")
		maxTxns := 3
		if len(statement.Transactions) < 3 {
			maxTxns = len(statement.Transactions)
		}
		for i := 0; i < maxTxns; i++ {
			txn := statement.Transactions[i]
			dateStr := "null"
			if txn.Date != nil {
				dateStr = *txn.Date
			}
			descStr := "null"
			if txn.Description != nil {
				descStr = *txn.Description
			}
			amountStr := "null"
			if txn.Amount != nil {
				amountStr = fmt.Sprintf("%.2f", *txn.Amount)
			}
			refStr := "null"
			if txn.ReferenceNumber != nil {
				refStr = *txn.ReferenceNumber
			}
			fmt.Printf("  %d. [%s] %s | $%s | Ref: %s\n", i+1, dateStr, descStr, amountStr, refStr)
		}
	}

	return statement, nil
}

func createParseRun(apiKey, dataURL string) (string, error) {
	reqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"blockOptions": map[string]interface{}{
				"text": map[string]interface{}{
					"agentic": map[string]bool{
						"enabled": true,
					},
				},
			},
			"chunkingStrategy": map[string]string{
				"type": "document",
			},
		},
	}

	bodyJSON, err := json.Marshal(reqBody)
	if err != nil {
		return "", err
	}

	req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/parse_runs", apiBaseURL), bytes.NewReader(bodyJSON))
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode >= 400 {
		body, _ := ioutil.ReadAll(resp.Body)
		return "", fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body))
	}

	var result map[string]interface{}
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return "", err
	}

	runID, ok := result["id"].(string)
	if !ok {
		return "", fmt.Errorf("missing run id in response")
	}

	return runID, nil
}

func pollParseRun(apiKey, runID string) (*parseRunResponse, error) {
	for {
		req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/parse_runs/%s", apiBaseURL, runID), nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))

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

		if resp.StatusCode >= 400 {
			body, _ := ioutil.ReadAll(resp.Body)
			return nil, fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body))
		}

		var result parseRunResponse
		err = json.NewDecoder(resp.Body).Decode(&result)
		if err != nil {
			return nil, err
		}

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

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

func createExtractRun(apiKey, dataURL string) (string, error) {
	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",
			},
			"account_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Bank account number",
			},
			"account_type": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Type of account (e.g., Student Checking)",
			},
			"statement_period_start": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Start date of statement period in format MMM DD, YYYY",
			},
			"statement_period_end": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "End date of statement period in format MMM DD, YYYY",
			},
			"beginning_balance": map[string]interface{}{
				"type":        []interface{}{"number", "null"},
				"description": "Account balance at the start of the statement period",
			},
			"ending_balance": map[string]interface{}{
				"type":        []interface{}{"number", "null"},
				"description": "Account balance at the end of the statement period",
			},
			"total_deposits_credits": map[string]interface{}{
				"type":        []interface{}{"number", "null"},
				"description": "Total amount of deposits and credits during the period",
			},
			"total_withdrawals": map[string]interface{}{
				"type":        []interface{}{"number", "null"},
				"description": "Total amount of all withdrawals during the period",
			},
			"account_holder_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Name of the account holder",
			},
			"account_holder_address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Mailing address of the account holder",
			},
			"transactions": map[string]interface{}{
				"type": "array",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"date": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Transaction date",
						},
						"description": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Transaction description",
						},
						"amount": map[string]interface{}{
							"type":        []interface{}{"number", "null"},
							"description": "Transaction amount",
						},
						"reference_number": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "Reference or confirmation number",
						},
					},
				},
				"description": "List of transactions",
			},
		},
	}

	reqBody := map[string]interface{}{
		"file": map[string]string{
			"url": dataURL,
		},
		"config": map[string]interface{}{
			"schema": schema,
			"advancedOptions": map[string]interface{}{
				"reviewAgent": map[string]bool{
					"enabled": true,
				},
				"advancedMultimodalEnabled": true,
			},
		},
	}

	bodyJSON, err := json.Marshal(reqBody)
	if err != nil {
		return "", err
	}

	req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/extract_runs", apiBaseURL), bytes.NewReader(bodyJSON))
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("Content-Type", "application/json")

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

	if resp.StatusCode >= 400 {
		body, _ := ioutil.ReadAll(resp.Body)
		return "", fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body))
	}

	var result map[string]interface{}
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return "", err
	}

	runID, ok := result["id"].(string)
	if !ok {
		return "", fmt.Errorf("missing run id in response")
	}

	return runID, nil
}

func pollExtractRun(apiKey, runID string) (*extractRunResponse, error) {
	for {
		req, err := http.NewRequest("GET", fmt.Sprintf("%s/v1/extract_runs/%s", apiBaseURL, runID), nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))

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

		if resp.StatusCode >= 400 {
			body, _ := ioutil.ReadAll(resp.Body)
			return nil, fmt.Errorf("API error: status %d, body: %s", resp.StatusCode, string(body))
		}

		var result extractRunResponse
		err = json.NewDecoder(resp.Body).Decode(&result)
		if err != nil {
			return nil, err
		}

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

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

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

	if len(args) == 0 {
		log.Fatal("Usage: go run . <bank_statement_file>")
	}

	filePath := args[0]
	result, err := processBankStatement(filePath)
	if err != nil {
		log.Fatalf("Error processing bank statement: %v", err)
	}

	fmt.Println("\nFinal output:")
	jsonOutput, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		log.Fatalf("Failed to marshal result: %v", err)
	}
	fmt.Println(string(jsonOutput))
}
// 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/personal-bank-statement.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   npx tsx provision.ts
//
// Generated by doc1 (template: personal-bank-statement).

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

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

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

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

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

// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
  "name": "Bank Statement Processing Pipeline",
  "steps": [
    {
      "name": "startTrigger1",
      "type": "TRIGGER",
      "next": [
        {
          "step": "parse1"
        }
      ]
    },
    {
      "name": "parse1",
      "type": "PARSE",
      "config": {
        "parseConfig": {
          "blockOptions": {
            "text": {
              "agentic": {
                "enabled": true
              }
            }
          },
          "chunkingStrategy": {
            "type": "document"
          }
        }
      },
      "next": [
        {
          "step": "extraction2"
        }
      ]
    },
    {
      "name": "extraction2",
      "type": "EXTRACT",
      "config": {
        "extractorConfig": {
          "schema": {
            "type": "object",
            "properties": {
              "bank_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the bank issuing the statement"
              },
              "account_type": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Type of account (e.g., Student Checking)"
              },
              "transactions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "date": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Transaction date"
                    },
                    "amount": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Transaction amount"
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Transaction description"
                    },
                    "reference_number": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Reference or confirmation number"
                    }
                  }
                },
                "description": "List of transactions"
              },
              "account_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Bank account number"
              },
              "ending_balance": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Account balance at the end of the statement period"
              },
              "beginning_balance": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Account balance at the start of the statement period"
              },
              "total_withdrawals": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total amount of all withdrawals during the period"
              },
              "account_holder_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the account holder"
              },
              "statement_period_end": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "End date of statement period in format MMM DD, YYYY"
              },
              "account_holder_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Mailing address of the account holder"
              },
              "statement_period_start": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Start date of statement period in format MMM DD, YYYY"
              },
              "total_deposits_credits": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total amount of deposits and credits during the period"
              }
            }
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
};

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 / "personal-bank-statement.json"


def load_state() -> dict[str, Any]:
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text())
    return {}


def save_state(state: dict[str, Any]) -> 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"
                            },
                            "account_type": {
                                "type": ["string", "null"],
                                "description": "Type of account (e.g., Student Checking)"
                            },
                            "transactions": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "date": {
                                            "type": ["string", "null"],
                                            "description": "Transaction date"
                                        },
                                        "amount": {
                                            "type": ["number", "null"],
                                            "description": "Transaction amount"
                                        },
                                        "description": {
                                            "type": ["string", "null"],
                                            "description": "Transaction description"
                                        },
                                        "reference_number": {
                                            "type": ["string", "null"],
                                            "description": "Reference or confirmation number"
                                        }
                                    }
                                },
                                "description": "List of transactions"
                            },
                            "account_number": {
                                "type": ["string", "null"],
                                "description": "Bank account number"
                            },
                            "ending_balance": {
                                "type": ["number", "null"],
                                "description": "Account balance at the end of the statement period"
                            },
                            "beginning_balance": {
                                "type": ["number", "null"],
                                "description": "Account balance at the start of the statement period"
                            },
                            "total_withdrawals": {
                                "type": ["number", "null"],
                                "description": "Total amount of all withdrawals during the period"
                            },
                            "account_holder_name": {
                                "type": ["string", "null"],
                                "description": "Name of the account holder"
                            },
                            "statement_period_end": {
                                "type": ["string", "null"],
                                "description": "End date of statement period in format MMM DD, YYYY"
                            },
                            "account_holder_address": {
                                "type": ["string", "null"],
                                "description": "Mailing address of the account holder"
                            },
                            "statement_period_start": {
                                "type": ["string", "null"],
                                "description": "Start date of statement period in format MMM DD, YYYY"
                            },
                            "total_deposits_credits": {
                                "type": ["number", "null"],
                                "description": "Total amount of deposits and credits during the period"
                            }
                        }
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {
                            "enabled": True
                        },
                        "advancedMultimodalEnabled": True
                    }
                }
            }
        }
    ]
}


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

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

    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        await client.workflows.update(workflow_id, {"steps": WORKFLOW["steps"]})
    else:
        # Try to find an existing workflow with the same name
        existing_id: Optional[str] = None
        try:
            workflows = await client.workflows.list(name=WORKFLOW["name"])
            items = getattr(workflows, "data", None) or getattr(workflows, "items", None) or []
            for item in items:
                if getattr(item, "name", None) == WORKFLOW["name"]:
                    existing_id = getattr(item, "id", None)
                    break
            if existing_id:
                state["workflowId"] = existing_id
                save_state(state)
                print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing_id}) — updating steps')
                await client.workflows.update(existing_id, {"steps": WORKFLOW["steps"]})
        except Exception:
            # lookup is best-effort; fall through to create
            pass

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

    # Deploy the current draft as a new version so the workflow is runnable
    workflow_id = state["workflowId"]
    try:
        await client.workflows.create_version(workflow_id, {})
    except Exception:
        # best-effort: some accounts/plans may not require this explicit step
        pass

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


if __name__ == "__main__":
    import asyncio
    try:
        asyncio.run(main())
    except Exception as e:
        print(f"Error: {str(e) if str(e) else e}", file=sys.stderr)
        sys.exit(1)
// This script uses the Extend REST API directly because Extend has no official Java SDK yet.
// Call the API directly via java.net.http.HttpClient (built-in, zero external dependencies).

import java.io.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;

public class BankStatementProvisioning {
  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("personal-bank-statement.json");

  static class State {
    public String workflowId;
  }

  private static State state = new State();
  private static final HttpClient httpClient = HttpClient.newHttpClient();

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

      loadState();

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

      if (state.workflowId != null && !state.workflowId.isEmpty()) {
        System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
        Map<String, Object> updateBody = new HashMap<>();
        updateBody.put("steps", workflow.get("steps"));
        api("POST", "/workflows/" + state.workflowId, updateBody);
      } else {
        // Reuse existing workflow with same name if found
        try {
          String listPath = "/workflows?name=" + URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
          Map<String, Object> listResp = api("GET", listPath, null);
          List<Map<String, Object>> items = null;

          if (listResp.containsKey("data")) {
            items = (List<Map<String, Object>>) listResp.get("data");
          } else if (listResp.containsKey("items")) {
            items = (List<Map<String, Object>>) listResp.get("items");
          } else {
            items = new ArrayList<>();
          }

          for (Map<String, Object> item : items) {
            if (workflowName.equals(item.get("name"))) {
              String existingId = (String) item.get("id");
              if (existingId != null) {
                state.workflowId = existingId;
                saveState();
                System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
                Map<String, Object> updateBody = new HashMap<>();
                updateBody.put("steps", workflow.get("steps"));
                api("POST", "/workflows/" + existingId, 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<String, Object> workflowObj = (Map<String, Object>) created.get("workflow");
            if (workflowObj != null) {
              wfId = (String) workflowObj.get("id");
            }
          }
          if (wfId == null) {
            throw new Exception("Could not read created workflow id from response");
          }
          state.workflowId = wfId;
          saveState();
          System.out.println("+ created workflow (" + wfId + ")");
        }
      }

      // Deploy the current draft as a new version — best-effort
      try {
        api("POST", "/workflows/" + state.workflowId + "/versions", new HashMap<>());
      } catch (Exception e) {
        // best-effort; ignore errors
      }

      System.out.println("\nDone. Run documents through it with:");
      System.out.println("  POST " + API + "/workflow_runs  { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
      System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
    } catch (Exception e) {
      System.err.println(e.getMessage() != null ? e.getMessage() : e);
      System.exit(1);
    }
  }

  private static void loadState() throws IOException {
    if (Files.exists(STATE_FILE)) {
      String content = Files.readString(STATE_FILE);
      Map<String, Object> parsed = parseJson(content);
      if (parsed.containsKey("workflowId")) {
        state.workflowId = (String) parsed.get("workflowId");
      }
    }
  }

  private static void saveState() throws IOException {
    Files.createDirectories(STATE_DIR);
    Map<String, Object> toSave = new HashMap<>();
    if (state.workflowId != null) {
      toSave.put("workflowId", state.workflowId);
    }
    String json = toJson(toSave);
    Files.writeString(STATE_FILE, json);
  }

  private static Map<String, Object> api(String method, String pathName, Map<String, Object> body) throws Exception {
    HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
        .uri(new URI(API + pathName))
        .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) {
      reqBuilder.header("Content-Type", "application/json");
    }

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

    Map<String, Object> data = null;
    try {
      data = parseJson(response.body());
    } catch (Exception e) {
      data = new HashMap<>();
    }

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

    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 = new ArrayList<>();

    // Trigger step
    Map<String, Object> triggerStep = new LinkedHashMap<>();
    triggerStep.put("name", "startTrigger1");
    triggerStep.put("type", "TRIGGER");
    List<Map<String, String>> triggerNext = new ArrayList<>();
    Map<String, String> triggerNextItem = new LinkedHashMap<>();
    triggerNextItem.put("step", "parse1");
    triggerNext.add(triggerNextItem);
    triggerStep.put("next", triggerNext);
    steps.add(triggerStep);

    // Parse step
    Map<String, Object> parseStep = new LinkedHashMap<>();
    parseStep.put("name", "parse1");
    parseStep.put("type", "PARSE");
    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> parseConfigInner = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();
    Map<String, Object> textBlock = new LinkedHashMap<>();
    Map<String, Object> agenticConfig = new LinkedHashMap<>();
    agenticConfig.put("enabled", true);
    textBlock.put("agentic", agenticConfig);
    blockOptions.put("text", textBlock);
    parseConfigInner.put("blockOptions", blockOptions);
    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "document");
    parseConfigInner.put("chunkingStrategy", chunkingStrategy);
    parseConfig.put("parseConfig", parseConfigInner);
    parseStep.put("config", parseConfig);
    List<Map<String, String>> parseNext = new ArrayList<>();
    Map<String, String> parseNextItem = new LinkedHashMap<>();
    parseNextItem.put("step", "extraction2");
    parseNext.add(parseNextItem);
    parseStep.put("next", parseNext);
    steps.add(parseStep);

    // Extract step
    Map<String, Object> extractStep = new LinkedHashMap<>();
    extractStep.put("name", "extraction2");
    extractStep.put("type", "EXTRACT");
    Map<String, Object> extractConfig = new LinkedHashMap<>();
    Map<String, Object> extractorConfig = new LinkedHashMap<>();
    Map<String, Object> schema = buildSchema();
    extractorConfig.put("schema", schema);
    extractorConfig.put("baseProcessor", "extraction_performance");
    Map<String, Object> advancedOptions = new LinkedHashMap<>();
    Map<String, Object> reviewAgent = new LinkedHashMap<>();
    reviewAgent.put("enabled", true);
    advancedOptions.put("reviewAgent", reviewAgent);
    advancedOptions.put("advancedMultimodalEnabled", true);
    extractorConfig.put("advancedOptions", advancedOptions);
    extractConfig.put("extractorConfig", extractorConfig);
    extractStep.put("config", extractConfig);
    steps.add(extractStep);

    workflow.put("steps", steps);
    return workflow;
  }

  private static Map<String, Object> buildSchema() {
    Map<String, Object> schema = new LinkedHashMap<>();
    schema.put("type", "object");

    Map<String, Object> properties = new LinkedHashMap<>();

    properties.put("bank_name", property("string", "Name of the bank issuing the statement"));
    properties.put("account_type", property("string", "Type of account (e.g., Student Checking)"));

    // Transactions array
    Map<String, Object> transactionsItem = new LinkedHashMap<>();
    transactionsItem.put("type", "object");
    Map<String, Object> transactionProps = new LinkedHashMap<>();
    transactionProps.put("date", property("string", "Transaction date"));
    transactionProps.put("amount", property("number", "Transaction amount"));
    transactionProps.put("description", property("string", "Transaction description"));
    transactionProps.put("reference_number", property("string", "Reference or confirmation number"));
    transactionsItem.put("properties", transactionProps);
    Map<String, Object> transactions = new LinkedHashMap<>();
    transactions.put("type", "array");
    transactions.put("items", transactionsItem);
    transactions.put("description", "List of transactions");
    properties.put("transactions", transactions);

    properties.put("account_number", property("string", "Bank account number"));
    properties.put("ending_balance", property("number", "Account balance at the end of the statement period"));
    properties.put("beginning_balance", property("number", "Account balance at the start of the statement period"));
    properties.put("total_withdrawals", property("number", "Total amount of all withdrawals during the period"));
    properties.put("account_holder_name", property("string", "Name of the account holder"));
    properties.put("statement_period_end", property("string", "End date of statement period in format MMM DD, YYYY"));
    properties.put("account_holder_address", property("string", "Mailing address of the account holder"));
    properties.put("statement_period_start", property("string", "Start date of statement period in format MMM DD, YYYY"));
    properties.put("total_deposits_credits", property("number", "Total amount of deposits and credits during the period"));

    schema.put("properties", properties);
    return schema;
  }

  private static Map<String, Object> property(String type, String description) {
    Map<String, Object> prop = new LinkedHashMap<>();
    List<Object> types = new ArrayList<>();
    types.add(type);
    types.add(null);
    prop.put("type", types);
    prop.put("description", description);
    return prop;
  }

  private static String toJson(Map<String, Object> map) {
    StringBuilder sb = new StringBuilder();
    sb.append("{");
    boolean first = true;
    for (Map.Entry<String, Object> entry : map.entrySet()) {
      if (!first) sb.append(", ");
      first = false;
      sb.append("\"").append(escapeJson(entry.getKey())).append("\": ");
      sb.append(valueToJson(entry.getValue()));
    }
    sb.append("}");
    return sb.toString();
  }

  private static String valueToJson(Object value) {
    if (value == null) {
      return "null";
    } else if (value instanceof String) {
      return "\"" + escapeJson((String) value) + "\"";
    } else if (value instanceof Boolean) {
      return value.toString();
    } else if (value instanceof Number) {
      return value.toString();
    } else if (value instanceof Map) {
      return toJson((Map<String, Object>) value);
    } else if (value instanceof List) {
      StringBuilder sb = new StringBuilder("[");
      boolean first = true;
      for (Object item : (List<?>) value) {
        if (!first) sb.append(", ");
        first = false;
        sb.append(valueToJson(item));
      }
      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) throws Exception {
    json = json.trim();
    if (!json.startsWith("{")) throw new Exception("Invalid JSON");

    Map<String, Object> result = new LinkedHashMap<>();
    int depth = 0;
    boolean inString = false;
    boolean escaped = false;
    StringBuilder currentKey = new StringBuilder();
    StringBuilder currentValue = new StringBuilder();
    boolean parsingKey = true;

    for (int i = 1; i < json.length() - 1; i++) {
      char c = json.charAt(i);

      if (escaped) {
        if (parsingKey) currentKey.append(c);
        else currentValue.append(c);
        escaped = false;
        continue;
      }

      if (c == '\\') {
        escaped = true;
        continue;
      }

      if (c == '"') {
        inString = !inString;
        if (!inString && parsingKey) {
          parsingKey = false;
        }
        continue;
      }

      if (inString) {
        if (parsingKey) currentKey.append(c);
        else currentValue.append(c);
        continue;
      }

      if (c == ':') {
        continue;
      }

      if (c == ',' && depth == 0) {
        String key = currentKey.toString().trim();
        String val = currentValue.toString().trim();
        result.put(key, parseJsonValue(val));
        currentKey = new StringBuilder();
        currentValue = new StringBuilder();
        parsingKey = true;
        continue;
      }

      if (c == '{' || c == '[') depth++;
      if (c == '}' || c == ']') depth--;

      currentValue.append(c);
    }

    if (currentKey.length() > 0) {
      String key = currentKey.toString().trim();
      String val = currentValue.toString().trim();
      result.put(key, parseJsonValue(val));
    }

    return result;
  }

  private static Object parseJsonValue(String val) throws Exception {
    val = val.trim();
    if (val.equals("null")) return null;
    if (val.equals("true")) return true;
    if (val.equals("false")) return false;
    if (val.startsWith("\"") && val.endsWith("\"")) {
      return val.substring(1, val.length() - 1);
    }
    if (val.startsWith("{")) return parseJson(val);
    if (val.startsWith("[")) {
      List<Object> list = new ArrayList<>();
      int depth = 0;
      StringBuilder item = new StringBuilder();
      boolean inString = false;
      for (int i = 1; i < val.length() - 1; i++) {
        char c = val.charAt(i);
        if (c == '"') inString = !inString;
        if (!inString && (c == '{' || c == '[')) depth++;
        if (!inString && (c == '}' || c == ']')) depth--;
        if (!inString && c == ',' && depth == 0) {
          list.add(parseJsonValue(item.toString()));
          item = new StringBuilder();
        } else {
          item.append(c);
        }
      }
      if (item.length() > 0) list.add(parseJsonValue(item.toString()));
      return list;
    }
    try {
      if (val.contains(".")) return Double.parseDouble(val);
      return Long.parseLong(val);
    } catch (Exception e) {
      return val;
    }
  }
}
package main

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

// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// All operations mirror the TypeScript reference exactly: same endpoints, same JSON shapes.

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

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

var (
	apiKey  string
	stateDir string
	stateFile string
	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)
	}

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

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

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

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

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

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

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

	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
}

func main() {
	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":        []interface{}{"string", nil},
									"description": "Name of the bank issuing the statement",
								},
								"account_type": map[string]interface{}{
									"type":        []interface{}{"string", nil},
									"description": "Type of account (e.g., Student Checking)",
								},
								"transactions": map[string]interface{}{
									"type": "array",
									"items": map[string]interface{}{
										"type": "object",
										"properties": map[string]interface{}{
											"date": map[string]interface{}{
												"type":        []interface{}{"string", nil},
												"description": "Transaction date",
											},
											"amount": map[string]interface{}{
												"type":        []interface{}{"number", nil},
												"description": "Transaction amount",
											},
											"description": map[string]interface{}{
												"type":        []interface{}{"string", nil},
												"description": "Transaction description",
											},
											"reference_number": map[string]interface{}{
												"type":        []interface{}{"string", nil},
												"description": "Reference or confirmation number",
											},
										},
									},
									"description": "List of transactions",
								},
								"account_number": map[string]interface{}{
									"type":        []interface{}{"string", nil},
									"description": "Bank account number",
								},
								"ending_balance": map[string]interface{}{
									"type":        []interface{}{"number", nil},
									"description": "Account balance at the end of the statement period",
								},
								"beginning_balance": map[string]interface{}{
									"type":        []interface{}{"number", nil},
									"description": "Account balance at the start of the statement period",
								},
								"total_withdrawals": map[string]interface{}{
									"type":        []interface{}{"number", nil},
									"description": "Total amount of all withdrawals during the period",
								},
								"account_holder_name": map[string]interface{}{
									"type":        []interface{}{"string", nil},
									"description": "Name of the account holder",
								},
								"statement_period_end": map[string]interface{}{
									"type":        []interface{}{"string", nil},
									"description": "End date of statement period in format MMM DD, YYYY",
								},
								"account_holder_address": map[string]interface{}{
									"type":        []interface{}{"string", nil},
									"description": "Mailing address of the account holder",
								},
								"statement_period_start": map[string]interface{}{
									"type":        []interface{}{"string", nil},
									"description": "Start date of statement period in format MMM DD, YYYY",
								},
								"total_deposits_credits": map[string]interface{}{
									"type":        []interface{}{"number", nil},
									"description": "Total amount of deposits and credits during the period",
								},
							},
						},
						"baseProcessor": "extraction_performance",
						"advancedOptions": map[string]interface{}{
							"reviewAgent": map[string]interface{}{
								"enabled": true,
							},
							"advancedMultimodalEnabled": true,
						},
					},
				},
			},
		},
	}

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

	if state.WorkflowID != "" {
		fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
		steps := workflow["steps"]
		if _, err := apiCall("POST", "/workflows/"+state.WorkflowID, map[string]interface{}{"steps": steps}); err != nil {
			fmt.Fprintf(os.Stderr, "%v\n", err)
			os.Exit(1)
		}
	} else {
		found := false
		q := url.QueryEscape(name)
		list, err := apiCall("GET", "/workflows?name="+q, nil)
		if err == nil {
			var items []map[string]interface{}
			if data, ok := list["data"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			} else if data, ok := list["items"].([]interface{}); ok {
				for _, item := range data {
					items = append(items, item.(map[string]interface{}))
				}
			}

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

		if !found {
			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)
		}
	}

	apiCall("POST", "/workflows/"+state.WorkflowID+"/versions", map[string]interface{}{})

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

Frequently Asked Questions (FAQ)

Use two separate calls: first parse with `agentic_ocr` mode to get clean markdown, then extract against that markdown output. This gives you a reusable parsed document, lets you inspect intermediate output for debugging, and keeps concerns separated for maintainability.
The extract endpoint returns a confidence score (0–1) per field in the output; treat anything below 0.85 as requiring human review in production. For mission-critical fields like total_amount or account_number, set your threshold higher (0.95+) and always validate extracted numbers against a checksum or reconciliation step.
Use async polling with `parseRuns.createAndPoll()` and `extractRuns.createAndPoll()` instead of sync endpoints—they handle large batches without timeout. Process statements in parallel, cache parsed markdown for 24 hours so re-extractions don't re-parse, and use `baseProcessor: "extraction_light"` only if statements are consistently clean digital PDFs; otherwise stick with `extraction_performance` for reliability.
Tags
BankingAccount StatementFinancial RecordsTransactions
About this template

This onboarding package extractor capture essential financial account information including account numbers, statement periods, opening/closing balances, and itemized deposits and withdrawals. These documents are critical for personal finance management, reconciliation, and record-keeping purposes.

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

Relevant templates for Financial & Banking

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