Financial & BankingParse → Extract

Invoice Extractor

Extracts line items, quantities, pricing, and delivery details from invoices.

Ship it with Extend

Live pipeline

a real document, processed end to end · view only
Source documentInvoice Sysco Wasabi.jpg

Step-by-step

An invoice is a commercial document issued by a vendor to a customer that itemizes products or services provided, specifies quantities and pricing, applies applicable taxes and adjustments, and requests payment by a stated due date. This template takes in Invoice and outputs markdown (.md) capturing the invoice's full text and layout structure, and JSON (.json) with structured fields including line items, vendor and customer details, payment amounts, and terms per the extraction schema by using Extend's Parse, Extract primitives.

Input
Invoice
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 — 14 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": "Invoice Extractor 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",
            "required": [
              "notes",
              "due_date",
              "line_items",
              "tax_amount",
              "vendor_name",
              "invoice_date",
              "total_amount",
              "customer_name",
              "payment_terms",
              "invoice_number",
              "vendor_address",
              "vendor_contact",
              "subtotal_amount",
              "customer_address"
            ],
            "properties": {
              "notes": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Any additional comments, instructions, or messages included on the invoice. May include thank you notes, payment instructions, or other relevant information."
              },
              "due_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date by which payment for this invoice is expected. May be explicitly stated or derived from payment terms. Look for labels like 'Due Date', 'Payment Due', or similar.",
                "extend:type": "date"
              },
              "line_items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "total",
                    "quantity",
                    "unit_price",
                    "description"
                  ],
                  "properties": {
                    "total": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The total cost for this line item, typically calculated as quantity multiplied by unit price. May also include item-level adjustments."
                    },
                    "quantity": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The number of units, hours, or quantity for this line item. Can be a whole number or decimal, depending on the item."
                    },
                    "unit_price": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The price per single unit of this item before quantity multiplication. This is the base rate or price for one unit."
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A description of the product, service, or charge for this line item. May include item names, service descriptions, or product codes."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The individual products, services, or charges that make up this invoice. Each item typically includes a description, quantity, unit price, and total. Format may vary from tables to lists or other structures."
              },
              "tax_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of tax applied to this invoice. May include sales tax, VAT, or other applicable taxes. Look for labels such as 'Tax', 'VAT', or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "vendor_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the company or entity issuing this invoice and requesting payment. This is the billing party or service provider. May appear under headings such as 'From', 'Seller', or 'Vendor'."
              },
              "invoice_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date when this invoice or bill was created or issued. This is the official date of the document, used for accounting and payment term calculations. May appear with labels such as 'Date', 'Invoice Date', or similar.",
                "extend:type": "date"
              },
              "total_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The final amount owed by the customer, including all charges, taxes, and adjustments. This is the complete payment obligation. May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "customer_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the customer, client, or company being billed. This is the recipient of the invoice. May appear under headings such as 'Bill To', 'Customer', or 'Client'."
              },
              "payment_terms": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The terms specifying when payment is due and any conditions for payment. May include phrases like 'Net 30', 'Due on receipt', or other arrangements. Look for sections labeled 'Payment Terms', 'Terms', or similar."
              },
              "invoice_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The unique identifier assigned to this invoice or billing document. This is the primary reference number for the transaction and may include numbers, letters, or special characters. Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."
              },
              "vendor_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The full mailing address of the vendor or service provider issuing the invoice. This may include street address, city, state, postal code, and country. Typically found near the vendor name."
              },
              "vendor_contact": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Contact information for the vendor, such as phone number or email address. May appear near the vendor name or address, and can include multiple contact methods."
              },
              "subtotal_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The sum of all line item charges before taxes, discounts, or additional fees are applied. This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "customer_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The full mailing address of the customer or recipient of the invoice. This may include street address, city, state, postal code, and country. Typically found near the customer name."
              }
            },
            "additionalProperties": false
          },
          "baseProcessor": "extraction_performance",
          "advancedOptions": {
            "reviewAgent": {
              "enabled": true
            },
            "advancedMultimodalEnabled": true
          }
        }
      }
    }
  ]
}
# Invoice Extractor Processing — Extend AI Skill

## What this pipeline does

This pipeline converts invoice PDFs into structured JSON with complete financial data: vendor/customer info, invoice metadata, itemized line items with quantities and pricing, tax and subtotal amounts, and payment terms. It uses agentic OCR parsing followed by extraction with a comprehensive schema and review-agent validation to ensure accuracy on complex multi-line invoices with varied formatting.

## When to use this

- **Accounts payable automation**: Extract invoice data for bookkeeping systems, expense management tools, or AP workflow automation.
- **E-commerce order processing**: Parse customer invoices to populate billing records, revenue recognition, and tax reporting.
- **Financial reconciliation**: Validate vendor invoices against purchase orders by extracting line items, amounts, and payment terms for matching.
- **Invoice data lakes**: Bulk-digitize invoice archives into a queryable JSON database for audit, analytics, or compliance.
- **Multi-currency/multi-region invoices**: Handles VAT, sales tax, and non-USD currencies with ISO 4217 currency codes.

## Processor pipeline

### Step 1: Parse (agentic_ocr mode)
**Processor**: `parse_performance` with agentic OCR enabled  
**Purpose**: Convert PDF to markdown with high fidelity, preserving table structure and handling complex layouts.  
**Config**:
```json
{
  "blockOptions": {
    "text": {
      "agentic": {
        "enabled": true
      }
    }
  },
  "chunkingStrategy": {
    "type": "document"
  }
}
```
**Why**: Agentic OCR excels at invoices with logos, handwritten annotations, scanned images, or non-standard table layouts. Document-level chunking keeps the full invoice context together, avoiding fragmentation of line items or totals.

### Step 2: Extract (extraction_performance with review agent)
**Processor**: `extraction_performance` with `reviewAgent` enabled and `advancedMultimodalEnabled`  
**Purpose**: Extract 14 fields (vendor/customer details, dates, line items, pricing, tax, and notes) into a strongly-typed JSON object.  
**Config**:
```json
{
  "baseProcessor": "extraction_performance",
  "advancedOptions": {
    "reviewAgent": {
      "enabled": true
    },
    "advancedMultimodalEnabled": true
  }
}
```
**Why**: `extraction_performance` is tuned for financial documents with precise currency and date handling. The review agent re-validates extracted fields against the parsed markdown, catching misalignments (e.g., swapped amounts or missing items). `advancedMultimodalEnabled` leverages visual hints (bolding, colors, positioning) that PDF structure provides.

## TypeScript implementation

```typescript
import fs from "fs";
import { ExtendClient, extendDate, extendCurrency } from "extend-ai";
import { z } from "zod";

// Initialize Extend client with API key
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });

// Define the line item schema
const lineItemSchema = z.object({
  description: z.string().nullable().describe(
    "A description of the product, service, or charge for this line item. " +
    "May include item names, service descriptions, or product codes."
  ),
  quantity: z.number().nullable().describe(
    "The number of units, hours, or quantity for this line item. " +
    "Can be a whole number or decimal, depending on the item."
  ),
  unit_price: z.number().nullable().describe(
    "The price per single unit of this item before quantity multiplication. " +
    "This is the base rate or price for one unit."
  ),
  total: z.number().nullable().describe(
    "The total cost for this line item, typically calculated as quantity multiplied by unit price. " +
    "May also include item-level adjustments."
  ),
});

// Define the main extraction schema using Zod
const invoiceSchema = z.object({
  invoice_number: z.string().nullable().describe(
    "The unique identifier assigned to this invoice or billing document. " +
    "This is the primary reference number for the transaction and may include numbers, letters, or special characters. " +
    "Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."
  ),
  invoice_date: extendDate().describe(
    "The date when this invoice or bill was created or issued. " +
    "This is the official date of the document, used for accounting and payment term calculations. " +
    "May appear with labels such as 'Date', 'Invoice Date', or similar. Return ISO 8601 format (yyyy-mm-dd)."
  ),
  due_date: extendDate().describe(
    "The date by which payment for this invoice is expected. " +
    "May be explicitly stated or derived from payment terms. " +
    "Look for labels like 'Due Date', 'Payment Due', or similar. Return ISO 8601 format (yyyy-mm-dd)."
  ),
  vendor_name: z.string().nullable().describe(
    "The name of the company or entity issuing this invoice and requesting payment. " +
    "This is the billing party or service provider. " +
    "May appear under headings such as 'From', 'Seller', or 'Vendor'."
  ),
  vendor_address: z.string().nullable().describe(
    "The full mailing address of the vendor or service provider issuing the invoice. " +
    "This may include street address, city, state, postal code, and country. " +
    "Typically found near the vendor name."
  ),
  vendor_contact: z.string().nullable().describe(
    "Contact information for the vendor, such as phone number or email address. " +
    "May appear near the vendor name or address, and can include multiple contact methods."
  ),
  customer_name: z.string().nullable().describe(
    "The name of the customer, client, or company being billed. " +
    "This is the recipient of the invoice. " +
    "May appear under headings such as 'Bill To', 'Customer', or 'Client'."
  ),
  customer_address: z.string().nullable().describe(
    "The full mailing address of the customer or recipient of the invoice. " +
    "This may include street address, city, state, postal code, and country. " +
    "Typically found near the customer name."
  ),
  payment_terms: z.string().nullable().describe(
    "The terms specifying when payment is due and any conditions for payment. " +
    "May include phrases like 'Net 30', 'Due on receipt', or other arrangements. " +
    "Look for sections labeled 'Payment Terms', 'Terms', or similar."
  ),
  line_items: z.array(lineItemSchema).describe(
    "The individual products, services, or charges that make up this invoice. " +
    "Each item typically includes a description, quantity, unit price, and total. " +
    "Format may vary from tables to lists or other structures."
  ),
  subtotal_amount: extendCurrency().describe(
    "The sum of all line item charges before taxes, discounts, or additional fees are applied. " +
    "This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar."
  ),
  tax_amount: extendCurrency().describe(
    "The total amount of tax applied to this invoice. " +
    "May include sales tax, VAT, or other applicable taxes. " +
    "Look for labels such as 'Tax', 'VAT', or similar."
  ),
  total_amount: extendCurrency().describe(
    "The final amount owed by the customer, including all charges, taxes, and adjustments. " +
    "This is the complete payment obligation. " +
    "May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar."
  ),
  notes: z.string().nullable().describe(
    "Any additional comments, instructions, or messages included on the invoice. " +
    "May include thank you notes, payment instructions, or other relevant information."
  ),
});

// Type definition for the extracted invoice data
type Invoice = z.infer<typeof invoiceSchema>;

/**
 * Process an invoice PDF and extract structured data.
 * @param filePath - Path to the invoice PDF file
 * @returns Extracted invoice data
 */
export async function processInvoiceExtractor(filePath: string): Promise<Invoice> {
  console.log(`Processing invoice: ${filePath}`);

  // Read the file and convert to base64 data URL
  // (Extend SDK does not accept Node.js ReadStreams directly)
  const fileBuffer = fs.readFileSync(filePath);
  const base64Data = fileBuffer.toString("base64");
  const dataUrl = `data:application/pdf;base64,${base64Data}`;

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

  console.log(
    `Parsed ${parseRun.output.chunks.length} chunk(s) from invoice`
  );

  // Step 2: Extract structured fields with review agent validation
  console.log("Step 2: Extracting structured invoice data...");
  const extractRun = await client.extractRuns.createAndPoll({
    file: { url: dataUrl },
    config: {
      schema: invoiceSchema,
      advancedOptions: {
        reviewAgent: {
          enabled: true,
        },
        advancedMultimodalEnabled: true,
      },
    },
  });

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

  const invoice: Invoice = extractRun.output.value;

  // Log extraction results
  console.log("\n=== EXTRACTION RESULTS ===\n");
  console.log(`Invoice #: ${invoice.invoice_number}`);
  console.log(`Vendor: ${invoice.vendor_name}`);
  console.log(`Customer: ${invoice.customer_name}`);
  console.log(`Invoice Date: ${invoice.invoice_date}`);
  console.log(`Due Date: ${invoice.due_date}`);
  console.log(`Payment Terms: ${invoice.payment_terms}`);
  console.log(`\nLine Items (${invoice.line_items.length}):`);

  for (const item of invoice.line_items) {
    console.log(
      `  - ${item.description}: ${item.quantity} × $${item.unit_price} = $${item.total}`
    );
  }

  console.log(
    `\nSubtotal: ${invoice.subtotal_amount.iso_4217_currency_code} ${invoice.subtotal_amount.amount}`
  );
  console.log(
    `Tax: ${invoice.tax_amount.iso_4217_currency_code} ${invoice.tax_amount.amount}`
  );
  console.log(
    `Total Due: ${invoice.total_amount.iso_4217_currency_code} ${invoice.total_amount.amount}`
  );

  if (invoice.notes) {
    console.log(`\nNotes: ${invoice.notes}`);
  }

  console.log("\n=== END RESULTS ===\n");

  return invoice;
}

// Main entry point for CLI execution
const filePath = process.argv[2] || "./invoice.pdf";
processInvoiceExtractor(filePath)
  .then((result) => {
    console.log("Invoice extraction complete. Data:");
    console.log(JSON.stringify(result, null, 2));
  })
  .catch((err) => {
    console.error("Error processing invoice:", err);
    process.exit(1);
  });
```

<SDK_CODE>
import fs from "fs";
import { ExtendClient, extendDate, extendCurrency } from "extend-ai";
import { z } from "zod";

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

const lineItemSchema = z.object({
  description: z.string().nullable().describe(
    "A description of the product, service, or charge for this line item. " +
    "May include item names, service descriptions, or product codes."
  ),
  quantity: z.number().nullable().describe(
    "The number of units, hours, or quantity for this line item. " +
    "Can be a whole number or decimal, depending on the item."
  ),
  unit_price: z.number().nullable().describe(
    "The price per single unit of this item before quantity multiplication. " +
    "This is the base rate or price for one unit."
  ),
  total: z.number().nullable().describe(
    "The total cost for this line item, typically calculated as quantity multiplied by unit price. " +
    "May also include item-level adjustments."
  ),
});

const invoiceSchema = z.object({
  invoice_number: z.string().nullable().describe(
    "The unique identifier assigned to this invoice or billing document. " +
    "This is the primary reference number for the transaction and may include numbers, letters, or special characters. " +
    "Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."
  ),
  invoice_date: extendDate().describe(
    "The date when this invoice or bill was created or issued. " +
    "This is the official date of the document, used for accounting and payment term calculations. " +
    "May appear with labels such as 'Date', 'Invoice Date', or similar. Return ISO 8601 format (yyyy-mm-dd)."
  ),
  due_date: extendDate().describe(
    "The date by which payment for this invoice is expected. " +
    "May be explicitly stated or derived from payment terms. " +
    "Look for labels like 'Due Date', 'Payment Due', or similar. Return ISO 8601 format (yyyy-mm-dd)."
  ),
  vendor_name: z.string().nullable().describe(
    "The name of the company or entity issuing this invoice and requesting payment. " +
    "This is the billing party or service provider. " +
    "May appear under headings such as 'From', 'Seller', or 'Vendor'."
  ),
  vendor_address: z.string().nullable().describe(
    "The full mailing address of the vendor or service provider issuing the invoice. " +
    "This may include street address, city, state, postal code, and country. " +
    "Typically found near the vendor name."
  ),
  vendor_contact: z.string().nullable().describe(
    "Contact information for the vendor, such as phone number or email address. " +
    "May appear near the vendor name or address, and can include multiple contact methods."
  ),
  customer_name: z.string().nullable().describe(
    "The name of the customer, client, or company being billed. " +
    "This is the recipient of the invoice. " +
    "May appear under headings such as 'Bill To', 'Customer', or 'Client'."
  ),
  customer_address: z.string().nullable().describe(
    "The full mailing address of the customer or recipient of the invoice. " +
    "This may include street address, city, state, postal code, and country. " +
    "Typically found near the customer name."
  ),
  payment_terms: z.string().nullable().describe(
    "The terms specifying when payment is due and any conditions for payment. " +
    "May include phrases like 'Net 30', 'Due on receipt', or other arrangements. " +
    "Look for sections labeled 'Payment Terms', 'Terms', or similar."
  ),
  line_items: z.array(lineItemSchema).describe(
    "The individual products, services, or charges that make up this invoice. " +
    "Each item typically includes a description, quantity, unit price, and total. " +
    "Format may vary from tables to lists or other structures."
  ),
  subtotal_amount: extendCurrency().describe(
    "
/**
 * Why: Agentic OCR excels at invoices with logos, handwritten annotations, scanned images, or non-standard table layouts. Document-level chunking keeps the full invoice context together, avoiding fragmentation of line items or totals.
 *
 * Step 2: Extract (extraction_performance with review agent)
 * Processor: extraction_performance with reviewAgent enabled and advancedMultimodalEnabled
 * Purpose: Extract 14 fields (vendor/customer details, dates, line items, pricing, tax, and notes) into a strongly-typed JSON object.
 * Config:
 */
import os
from extend_ai import Extend

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

# Define the extraction schema for invoice data
extraction_schema = {
    "type": "object",
    "properties": {
        "notes": {
            "type": ["string", "null"],
            "description": "Any additional comments, instructions, or messages included on the invoice. May include thank you notes, payment instructions, or other relevant information."
        },
        "due_date": {
            "type": ["string", "null"],
            "extend:type": "date",
            "description": "The date by which payment for this invoice is expected. May be explicitly stated or derived from payment terms. Look for labels like 'Due Date', 'Payment Due', or similar."
        },
        "line_items": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "total": {
                        "type": ["number", "null"],
                        "description": "The total cost for this line item, typically calculated as quantity multiplied by unit price. May also include item-level adjustments."
                    },
                    "quantity": {
                        "type": ["number", "null"],
                        "description": "The number of units, hours, or quantity for this line item. Can be a whole number or decimal, depending on the item."
                    },
                    "unit_price": {
                        "type": ["number", "null"],
                        "description": "The price per single unit of this item before quantity multiplication. This is the base rate or price for one unit."
                    },
                    "description": {
                        "type": ["string", "null"],
                        "description": "A description of the product, service, or charge for this line item. May include item names, service descriptions, or product codes."
                    }
                },
                "additionalProperties": False,
                "required": ["total", "quantity", "unit_price", "description"]
            },
            "description": "The individual products, services, or charges that make up this invoice. Each item typically includes a description, quantity, unit price, and total. Format may vary from tables to lists or other structures."
        },
        "tax_amount": {
            "type": "object",
            "properties": {
                "amount": {
                    "type": ["number", "null"]
                },
                "iso_4217_currency_code": {
                    "type": ["string", "null"]
                }
            },
            "additionalProperties": False,
            "required": ["amount", "iso_4217_currency_code"],
            "extend:type": "currency",
            "description": "The total amount of tax applied to this invoice. May include sales tax, VAT, or other applicable taxes. Look for labels such as 'Tax', 'VAT', or similar."
        },
        "vendor_name": {
            "type": ["string", "null"],
            "description": "The name of the company or entity issuing this invoice and requesting payment. This is the billing party or service provider. May appear under headings such as 'From', 'Seller', or 'Vendor'."
        },
        "invoice_date": {
            "type": ["string", "null"],
            "extend:type": "date",
            "description": "The date when this invoice or bill was created or issued. This is the official date of the document, used for accounting and payment term calculations. May appear with labels such as 'Date', 'Invoice Date', or similar."
        },
        "total_amount": {
            "type": "object",
            "properties": {
                "amount": {
                    "type": ["number", "null"]
                },
                "iso_4217_currency_code": {
                    "type": ["string", "null"]
                }
            },
            "additionalProperties": False,
            "required": ["amount", "iso_4217_currency_code"],
            "extend:type": "currency",
            "description": "The final amount owed by the customer, including all charges, taxes, and adjustments. This is the complete payment obligation. May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar."
        },
        "customer_name": {
            "type": ["string", "null"],
            "description": "The name of the customer, client, or company being billed. This is the recipient of the invoice. May appear under headings such as 'Bill To', 'Customer', or 'Client'."
        },
        "payment_terms": {
            "type": ["string", "null"],
            "description": "The terms specifying when payment is due and any conditions for payment. May include phrases like 'Net 30', 'Due on receipt', or other arrangements. Look for sections labeled 'Payment Terms', 'Terms', or similar."
        },
        "invoice_number": {
            "type": ["string", "null"],
            "description": "The unique identifier assigned to this invoice or billing document. This is the primary reference number for the transaction and may include numbers, letters, or special characters. Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."
        },
        "vendor_address": {
            "type": ["string", "null"],
            "description": "The full mailing address of the vendor or service provider issuing the invoice. This may include street address, city, state, postal code, and country. Typically found near the vendor name."
        },
        "vendor_contact": {
            "type": ["string", "null"],
            "description": "Contact information for the vendor, such as phone number or email address. May appear near the vendor name or address, and can include multiple contact methods."
        },
        "subtotal_amount": {
            "type": "object",
            "properties": {
                "amount": {
                    "type": ["number", "null"]
                },
                "iso_4217_currency_code": {
                    "type": ["string", "null"]
                }
            },
            "additionalProperties": False,
            "required": ["amount", "iso_4217_currency_code"],
            "extend:type": "currency",
            "description": "The sum of all line item charges before taxes, discounts, or additional fees are applied. This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar."
        },
        "customer_address": {
            "type": ["string", "null"],
            "description": "The full mailing address of the customer or recipient of the invoice. This may include street address, city, state, postal code, and country. Typically found near the customer name."
        }
    },
    "required": [
        "notes",
        "due_date",
        "line_items",
        "tax_amount",
        "vendor_name",
        "invoice_date",
        "total_amount",
        "customer_name",
        "payment_terms",
        "invoice_number",
        "vendor_address",
        "vendor_contact",
        "subtotal_amount",
        "customer_address"
    ],
    "additionalProperties": False
}

# Configuration for extraction with review agent and advanced multimodal
extraction_config = {
    "processor": "extraction_performance",
    "review_agent": True,
    "advanced_multimodal_enabled": True,
    "schema": extraction_schema
}

# Example usage: upload a file and create an extraction run
def extract_invoice(file_path: str):
    # Upload the invoice file
    with open(file_path, "rb") as f:
        file = client.files.upload(file=f)
    
    # Create an extraction run with the configuration
    extract_run = client.extract_runs.create(
        file={"id": file.id},
        extractor={"config": extraction_config}
    )
    
    # Poll for completion
    completed_run = client.extract_runs.poll(extract_run.id)
    
    return completed_run
// This code uses Extend's REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints with Bearer token authentication.

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.charset.StandardCharsets;

public class InvoiceExtractor {

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

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        // Step 1: Parse (agentic OCR with document-level chunking)
        String parsePayload = """
            {
              "processor": "agentic_ocr",
              "config": {
                "chunkingStrategy": "document"
              },
              "document": {
                "type": "file",
                "path": "invoice.pdf"
              }
            }
            """;

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

        HttpResponse<String> parseResponse = client.send(parseRequest, HttpResponse.BodyHandlers.ofString());
        System.out.println("Parse Response: " + parseResponse.body());

        // Step 2: Extract (extraction_performance with review agent and advanced multimodal)
        String extractPayload = """
            {
              "processor": "extraction_performance",
              "config": {
                "reviewAgent": true,
                "advancedMultimodalEnabled": true,
                "schema": {
                  "type": "object",
                  "properties": {
                    "notes": {
                      "type": ["string", "null"],
                      "description": "Any additional comments, instructions, or messages included on the invoice."
                    },
                    "due_date": {
                      "type": ["string", "null"],
                      "extend:type": "date",
                      "description": "The date by which payment for this invoice is expected."
                    },
                    "line_items": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "total": {
                            "type": ["number", "null"],
                            "description": "The total cost for this line item."
                          },
                          "quantity": {
                            "type": ["number", "null"],
                            "description": "The number of units for this line item."
                          },
                          "unit_price": {
                            "type": ["number", "null"],
                            "description": "The price per single unit of this item."
                          },
                          "description": {
                            "type": ["string", "null"],
                            "description": "A description of the product or service."
                          }
                        },
                        "additionalProperties": false,
                        "required": ["total", "quantity", "unit_price", "description"]
                      },
                      "description": "The individual products, services, or charges that make up this invoice."
                    },
                    "tax_amount": {
                      "type": "object",
                      "properties": {
                        "amount": {"type": ["number", "null"]},
                        "iso_4217_currency_code": {"type": ["string", "null"]}
                      },
                      "additionalProperties": false,
                      "required": ["amount", "iso_4217_currency_code"],
                      "extend:type": "currency",
                      "description": "The total amount of tax applied to this invoice."
                    },
                    "vendor_name": {
                      "type": ["string", "null"],
                      "description": "The name of the company issuing this invoice."
                    },
                    "invoice_date": {
                      "type": ["string", "null"],
                      "extend:type": "date",
                      "description": "The date when this invoice was created or issued."
                    },
                    "total_amount": {
                      "type": "object",
                      "properties": {
                        "amount": {"type": ["number", "null"]},
                        "iso_4217_currency_code": {"type": ["string", "null"]}
                      },
                      "additionalProperties": false,
                      "required": ["amount", "iso_4217_currency_code"],
                      "extend:type": "currency",
                      "description": "The final amount owed by the customer."
                    },
                    "customer_name": {
                      "type": ["string", "null"],
                      "description": "The name of the customer being billed."
                    },
                    "payment_terms": {
                      "type": ["string", "null"],
                      "description": "The terms specifying when payment is due."
                    },
                    "invoice_number": {
                      "type": ["string", "null"],
                      "description": "The unique identifier assigned to this invoice."
                    },
                    "vendor_address": {
                      "type": ["string", "null"],
                      "description": "The full mailing address of the vendor."
                    },
                    "vendor_contact": {
                      "type": ["string", "null"],
                      "description": "Contact information for the vendor."
                    },
                    "subtotal_amount": {
                      "type": "object",
                      "properties": {
                        "amount": {"type": ["number", "null"]},
                        "iso_4217_currency_code": {"type": ["string", "null"]}
                      },
                      "additionalProperties": false,
                      "required": ["amount", "iso_4217_currency_code"],
                      "extend:type": "currency",
                      "description": "The sum of all line item charges before taxes."
                    },
                    "customer_address": {
                      "type": ["string", "null"],
                      "description": "The full mailing address of the customer."
                    }
                  },
                  "required": ["notes", "due_date", "line_items", "tax_amount", "vendor_name", "invoice_date", "total_amount", "customer_name", "payment_terms", "invoice_number", "vendor_address", "vendor_contact", "subtotal_amount", "customer_address"],
                  "additionalProperties": false
                }
              },
              "document": {
                "type": "file",
                "path": "invoice.pdf"
              }
            }
            """;

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

        HttpResponse<String> extractResponse = client.send(extractRequest, HttpResponse.BodyHandlers.ofString());
        System.out.println("Extract Response: " + extractResponse.body());
    }
}
// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.

package main

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

// LineItem represents a single line item on the invoice
type LineItem struct {
	Total       *float64 `json:"total"`
	Quantity    *float64 `json:"quantity"`
	UnitPrice   *float64 `json:"unit_price"`
	Description *string  `json:"description"`
}

// CurrencyAmount represents an amount with ISO 4217 currency code
type CurrencyAmount struct {
	Amount              *float64 `json:"amount"`
	ISO4217CurrencyCode *string  `json:"iso_4217_currency_code"`
}

// InvoiceData represents the extracted invoice fields
type InvoiceData struct {
	Notes           *string           `json:"notes"`
	DueDate         *string           `json:"due_date"`
	LineItems       []LineItem        `json:"line_items"`
	TaxAmount       CurrencyAmount    `json:"tax_amount"`
	VendorName      *string           `json:"vendor_name"`
	InvoiceDate     *string           `json:"invoice_date"`
	TotalAmount     CurrencyAmount    `json:"total_amount"`
	CustomerName    *string           `json:"customer_name"`
	PaymentTerms    *string           `json:"payment_terms"`
	InvoiceNumber   *string           `json:"invoice_number"`
	VendorAddress   *string           `json:"vendor_address"`
	VendorContact   *string           `json:"vendor_contact"`
	SubtotalAmount  CurrencyAmount    `json:"subtotal_amount"`
	CustomerAddress *string           `json:"customer_address"`
}

// ExtractRequest represents the extraction request payload
type ExtractRequest struct {
	DocumentID string      `json:"document_id"`
	Processor  string      `json:"processor"`
	Config     ExtractConfig `json:"config"`
}

// ExtractConfig contains extraction configuration
type ExtractConfig struct {
	ReviewAgent              bool   `json:"reviewAgent"`
	AdvancedMultimodalEnabled bool   `json:"advancedMultimodalEnabled"`
	Schema                   map[string]interface{} `json:"schema"`
}

// ExtractResponse represents the extraction response
type ExtractResponse struct {
	Data InvoiceData `json:"data"`
}

func main() {
	apiKey := os.Getenv("EXTEND_API_KEY")
	if apiKey == "" {
		fmt.Println("Error: EXTEND_API_KEY environment variable not set")
		os.Exit(1)
	}

	// Define the extraction schema
	schema := map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"notes": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Any additional comments, instructions, or messages included on the invoice.",
			},
			"due_date": map[string]interface{}{
				"type":           []string{"string", "null"},
				"extend:type":    "date",
				"description":    "The date by which payment for this invoice is expected.",
			},
			"line_items": map[string]interface{}{
				"type": "array",
				"items": map[string]interface{}{
					"type": "object",
					"properties": map[string]interface{}{
						"total": map[string]interface{}{
							"type":        []string{"number", "null"},
							"description": "The total cost for this line item.",
						},
						"quantity": map[string]interface{}{
							"type":        []string{"number", "null"},
							"description": "The number of units for this line item.",
						},
						"unit_price": map[string]interface{}{
							"type":        []string{"number", "null"},
							"description": "The price per single unit of this item.",
						},
						"description": map[string]interface{}{
							"type":        []string{"string", "null"},
							"description": "A description of the product or service.",
						},
					},
					"additionalProperties": false,
					"required":             []string{"total", "quantity", "unit_price", "description"},
				},
				"description": "The individual products, services, or charges that make up this invoice.",
			},
			"tax_amount": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]interface{}{
						"type": []string{"string", "null"},
					},
				},
				"additionalProperties": false,
				"required":             []string{"amount", "iso_4217_currency_code"},
				"extend:type":          "currency",
				"description":          "The total amount of tax applied to this invoice.",
			},
			"vendor_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The name of the company issuing this invoice.",
			},
			"invoice_date": map[string]interface{}{
				"type":        []string{"string", "null"},
				"extend:type": "date",
				"description": "The date when this invoice was created or issued.",
			},
			"total_amount": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]interface{}{
						"type": []string{"string", "null"},
					},
				},
				"additionalProperties": false,
				"required":             []string{"amount", "iso_4217_currency_code"},
				"extend:type":          "currency",
				"description":          "The final amount owed by the customer.",
			},
			"customer_name": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The name of the customer being billed.",
			},
			"payment_terms": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The terms specifying when payment is due.",
			},
			"invoice_number": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The unique identifier assigned to this invoice.",
			},
			"vendor_address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The full mailing address of the vendor.",
			},
			"vendor_contact": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "Contact information for the vendor.",
			},
			"subtotal_amount": map[string]interface{}{
				"type": "object",
				"properties": map[string]interface{}{
					"amount": map[string]interface{}{
						"type": []string{"number", "null"},
					},
					"iso_4217_currency_code": map[string]interface{}{
						"type": []string{"string", "null"},
					},
				},
				"additionalProperties": false,
				"required":             []string{"amount", "iso_4217_currency_code"},
				"extend:type":          "currency",
				"description":          "The sum of all line item charges before taxes.",
			},
			"customer_address": map[string]interface{}{
				"type":        []string{"string", "null"},
				"description": "The full mailing address of the customer.",
			},
		},
		"required": []string{
			"notes", "due_date", "line_items", "tax_amount", "vendor_name",
			"invoice_date", "total_amount", "customer_name", "payment_terms",
			"invoice_number", "vendor_address", "vendor_contact", "subtotal_amount",
			"customer_address",
		},
		"additionalProperties": false,
	}

	// Create extraction request
	extractReq := ExtractRequest{
		DocumentID: "your-document-id", // Replace with actual document ID
		Processor:  "extraction_performance",
		Config: ExtractConfig{
			ReviewAgent:              true,
			AdvancedMultimodalEnabled: true,
			Schema:                   schema,
		},
	}

	// Marshal request to JSON
	reqBody, err := json.Marshal(extractReq)
	if err != nil {
		fmt.Printf("Error marshaling request: %v\n", err)
		os.Exit(1)
	}

	// Create HTTP request
	req, err := http.NewRequest("POST", "https://api.extend.ai/v1/extract", bytes.NewBuffer(reqBody))
	if err != nil {
		fmt.Printf("Error creating request: %v\n", err)
		os.Exit(1)
	}

	// Set headers
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
	req.Header.Set("Content-Type", "application/json")

	// Send request
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("Error sending request: %v\n", err)
		os.Exit(1)
	}
	defer resp.Body.Close()

	// Read response body
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("Error reading response: %v\n", err)
		os.Exit(1)
	}

	// Check response status
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Error: API returned status %d\n%s\n", resp.StatusCode, string(respBody))
		os.Exit(1)
	}

	// Unmarshal response
	var extractResp ExtractResponse
	err = json.Unmarshal(respBody, &extractResp)
	if err != nil {
		fmt.Printf("Error unmarshaling response: %v\n", err)
		os.Exit(1)
	}

	// Print extracted data
	fmt.Printf("Extracted Invoice Data:\n")
	fmt.Printf("Invoice Number: %v\n", extractResp.Data.InvoiceNumber)
	fmt.Printf("Vendor Name: %v\n", extractResp.Data.VendorName)
	fmt.Printf("Customer Name: %v\n", extractResp.Data.CustomerName)
	fmt.Printf("Invoice Date: %v\n", extractResp.Data.InvoiceDate)
	fmt.Printf("Due Date: %v\n", extractResp.Data.DueDate)
	fmt.Printf("Total Amount: %v %v\n", extractResp.Data.TotalAmount.Amount, extractResp.Data.TotalAmount.ISO4217CurrencyCode)
	fmt.Printf("Tax Amount: %v %v\n", extractResp.Data.TaxAmount.Amount, extractResp.Data.TaxAmount.ISO4217CurrencyCode)
	fmt.Printf("Line Items Count: %d\n", len(extractResp.Data.LineItems))
	fmt.Printf("Payment Terms: %v\n", extractResp.Data.PaymentTerms)
	fmt.Printf("Notes: %v\n", extractResp.Data.Notes)
}
// Deploy the "Invoice Extractor" 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/invoice-extractor.json,
// so re-running updates the existing workflow instead of duplicating it.
//
// Usage:
//   export EXTEND_API_KEY=sk_...   (from https://dashboard.extend.ai → API Keys)
//   npx tsx provision.ts
//
// Generated by doc1 (template: invoice-extractor).

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

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

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

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

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

// ── Workflow definition — extractor/classifier/splitter configs inline ──────
const WORKFLOW = {
  "name": "Invoice Extractor 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",
            "required": [
              "notes",
              "due_date",
              "line_items",
              "tax_amount",
              "vendor_name",
              "invoice_date",
              "total_amount",
              "customer_name",
              "payment_terms",
              "invoice_number",
              "vendor_address",
              "vendor_contact",
              "subtotal_amount",
              "customer_address"
            ],
            "properties": {
              "notes": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Any additional comments, instructions, or messages included on the invoice. May include thank you notes, payment instructions, or other relevant information."
              },
              "due_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date by which payment for this invoice is expected. May be explicitly stated or derived from payment terms. Look for labels like 'Due Date', 'Payment Due', or similar.",
                "extend:type": "date"
              },
              "line_items": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "total",
                    "quantity",
                    "unit_price",
                    "description"
                  ],
                  "properties": {
                    "total": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The total cost for this line item, typically calculated as quantity multiplied by unit price. May also include item-level adjustments."
                    },
                    "quantity": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The number of units, hours, or quantity for this line item. Can be a whole number or decimal, depending on the item."
                    },
                    "unit_price": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "The price per single unit of this item before quantity multiplication. This is the base rate or price for one unit."
                    },
                    "description": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A description of the product, service, or charge for this line item. May include item names, service descriptions, or product codes."
                    }
                  },
                  "additionalProperties": false
                },
                "description": "The individual products, services, or charges that make up this invoice. Each item typically includes a description, quantity, unit price, and total. Format may vary from tables to lists or other structures."
              },
              "tax_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The total amount of tax applied to this invoice. May include sales tax, VAT, or other applicable taxes. Look for labels such as 'Tax', 'VAT', or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "vendor_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the company or entity issuing this invoice and requesting payment. This is the billing party or service provider. May appear under headings such as 'From', 'Seller', or 'Vendor'."
              },
              "invoice_date": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The date when this invoice or bill was created or issued. This is the official date of the document, used for accounting and payment term calculations. May appear with labels such as 'Date', 'Invoice Date', or similar.",
                "extend:type": "date"
              },
              "total_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The final amount owed by the customer, including all charges, taxes, and adjustments. This is the complete payment obligation. May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "customer_name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The name of the customer, client, or company being billed. This is the recipient of the invoice. May appear under headings such as 'Bill To', 'Customer', or 'Client'."
              },
              "payment_terms": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The terms specifying when payment is due and any conditions for payment. May include phrases like 'Net 30', 'Due on receipt', or other arrangements. Look for sections labeled 'Payment Terms', 'Terms', or similar."
              },
              "invoice_number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The unique identifier assigned to this invoice or billing document. This is the primary reference number for the transaction and may include numbers, letters, or special characters. Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."
              },
              "vendor_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The full mailing address of the vendor or service provider issuing the invoice. This may include street address, city, state, postal code, and country. Typically found near the vendor name."
              },
              "vendor_contact": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Contact information for the vendor, such as phone number or email address. May appear near the vendor name or address, and can include multiple contact methods."
              },
              "subtotal_amount": {
                "type": "object",
                "required": [
                  "amount",
                  "iso_4217_currency_code"
                ],
                "properties": {
                  "amount": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "iso_4217_currency_code": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "description": "The sum of all line item charges before taxes, discounts, or additional fees are applied. This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar.",
                "extend:type": "currency",
                "additionalProperties": false
              },
              "customer_address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The full mailing address of the customer or recipient of the invoice. This may include street address, city, state, postal code, and country. Typically found near the customer name."
              }
            },
            "additionalProperties": false
          },
          "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 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 / "invoice-extractor.json"

state = {}
if STATE_FILE.exists():
    state = json.loads(STATE_FILE.read_text())

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

WORKFLOW = {
    "name": "Invoice Extractor 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": {
                            "notes": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Any additional comments, instructions, or messages included on the invoice. May include thank you notes, payment instructions, or other relevant information."
                            },
                            "due_date": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "extend:type": "date",
                                "description": "The date by which payment for this invoice is expected. May be explicitly stated or derived from payment terms. Look for labels like 'Due Date', 'Payment Due', or similar."
                            },
                            "line_items": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "total": {
                                            "type": [
                                                "number",
                                                "null"
                                            ],
                                            "description": "The total cost for this line item, typically calculated as quantity multiplied by unit price. May also include item-level adjustments."
                                        },
                                        "quantity": {
                                            "type": [
                                                "number",
                                                "null"
                                            ],
                                            "description": "The number of units, hours, or quantity for this line item. Can be a whole number or decimal, depending on the item."
                                        },
                                        "unit_price": {
                                            "type": [
                                                "number",
                                                "null"
                                            ],
                                            "description": "The price per single unit of this item before quantity multiplication. This is the base rate or price for one unit."
                                        },
                                        "description": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "A description of the product, service, or charge for this line item. May include item names, service descriptions, or product codes."
                                        }
                                    },
                                    "additionalProperties": False,
                                    "required": [
                                        "total",
                                        "quantity",
                                        "unit_price",
                                        "description"
                                    ]
                                },
                                "description": "The individual products, services, or charges that make up this invoice. Each item typically includes a description, quantity, unit price, and total. Format may vary from tables to lists or other structures."
                            },
                            "tax_amount": {
                                "type": "object",
                                "properties": {
                                    "amount": {
                                        "type": [
                                            "number",
                                            "null"
                                        ]
                                    },
                                    "iso_4217_currency_code": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    }
                                },
                                "additionalProperties": False,
                                "required": [
                                    "amount",
                                    "iso_4217_currency_code"
                                ],
                                "extend:type": "currency",
                                "description": "The total amount of tax applied to this invoice. May include sales tax, VAT, or other applicable taxes. Look for labels such as 'Tax', 'VAT', or similar."
                            },
                            "vendor_name": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The name of the company or entity issuing this invoice and requesting payment. This is the billing party or service provider. May appear under headings such as 'From', 'Seller', or 'Vendor'."
                            },
                            "invoice_date": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "extend:type": "date",
                                "description": "The date when this invoice or bill was created or issued. This is the official date of the document, used for accounting and payment term calculations. May appear with labels such as 'Date', 'Invoice Date', or similar."
                            },
                            "total_amount": {
                                "type": "object",
                                "properties": {
                                    "amount": {
                                        "type": [
                                            "number",
                                            "null"
                                        ]
                                    },
                                    "iso_4217_currency_code": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    }
                                },
                                "additionalProperties": False,
                                "required": [
                                    "amount",
                                    "iso_4217_currency_code"
                                ],
                                "extend:type": "currency",
                                "description": "The final amount owed by the customer, including all charges, taxes, and adjustments. This is the complete payment obligation. May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar."
                            },
                            "customer_name": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The name of the customer, client, or company being billed. This is the recipient of the invoice. May appear under headings such as 'Bill To', 'Customer', or 'Client'."
                            },
                            "payment_terms": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The terms specifying when payment is due and any conditions for payment. May include phrases like 'Net 30', 'Due on receipt', or other arrangements. Look for sections labeled 'Payment Terms', 'Terms', or similar."
                            },
                            "invoice_number": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The unique identifier assigned to this invoice or billing document. This is the primary reference number for the transaction and may include numbers, letters, or special characters. Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."
                            },
                            "vendor_address": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The full mailing address of the vendor or service provider issuing the invoice. This may include street address, city, state, postal code, and country. Typically found near the vendor name."
                            },
                            "vendor_contact": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "Contact information for the vendor, such as phone number or email address. May appear near the vendor name or address, and can include multiple contact methods."
                            },
                            "subtotal_amount": {
                                "type": "object",
                                "properties": {
                                    "amount": {
                                        "type": [
                                            "number",
                                            "null"
                                        ]
                                    },
                                    "iso_4217_currency_code": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    }
                                },
                                "additionalProperties": False,
                                "required": [
                                    "amount",
                                    "iso_4217_currency_code"
                                ],
                                "extend:type": "currency",
                                "description": "The sum of all line item charges before taxes, discounts, or additional fees are applied. This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar."
                            },
                            "customer_address": {
                                "type": [
                                    "string",
                                    "null"
                                ],
                                "description": "The full mailing address of the customer or recipient of the invoice. This may include street address, city, state, postal code, and country. Typically found near the customer name."
                            }
                        },
                        "required": [
                            "notes",
                            "due_date",
                            "line_items",
                            "tax_amount",
                            "vendor_name",
                            "invoice_date",
                            "total_amount",
                            "customer_name",
                            "payment_terms",
                            "invoice_number",
                            "vendor_address",
                            "vendor_contact",
                            "subtotal_amount",
                            "customer_address"
                        ],
                        "additionalProperties": False
                    },
                    "baseProcessor": "extraction_performance",
                    "advancedOptions": {
                        "reviewAgent": {
                            "enabled": True
                        },
                        "advancedMultimodalEnabled": True
                    }
                }
            }
        }
    ]
}

def main():
    client = Extend(token=API_KEY)
    
    print(f"Deploying \"{WORKFLOW['name']}\"…")
    
    if state.get("workflowId"):
        workflow_id = state["workflowId"]
        print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
        client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
    else:
        try:
            workflows_list = client.workflows.list(name=WORKFLOW["name"])
            items = workflows_list.data if hasattr(workflows_list, "data") else (workflows_list.items if hasattr(workflows_list, "items") else [])
            existing = next((x for x in items if x.name == WORKFLOW["name"]), None)
            if existing and existing.id:
                state["workflowId"] = existing.id
                save_state()
                print(f"✓ workflow \"{WORKFLOW['name']}\" found in your account ({existing.id}) — updating steps")
                client.workflows.update(id=existing.id, steps=WORKFLOW["steps"])
        except Exception:
            pass
        
        if not state.get("workflowId"):
            created = client.workflows.create(**WORKFLOW)
            workflow_id = created.id if hasattr(created, "id") else (created.workflow.id if hasattr(created, "workflow") else None)
            if not workflow_id:
                raise ValueError("Could not read created workflow id from response")
            state["workflowId"] = workflow_id
            save_state()
            print(f"+ created workflow ({workflow_id})")
    
    try:
        client.workflows.create_version(id=state["workflowId"])
    except Exception:
        pass
    
    print("\nDone. Run documents through it with:")
    print(f"  POST https://api.extend.ai/workflow_runs  {{ \"workflow\": {{ \"id\": \"{state['workflowId']}\" }}, \"file\": {{ \"url\": \"https://…\" }} }}")
    print("Or open the workflow in the Extend dashboard to review and deploy it.")

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

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

public class InvoiceExtractorProvisioner {
  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("invoice-extractor.json");

  private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

  static class State {
    String workflowId;
  }

  private static State state = new State();

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

    loadState();

    Map<String, Object> workflow = buildWorkflow();
    String workflowName = (String) workflow.get("name");

    System.out.println("Deploying \"" + workflowName + "\"…");

    @SuppressWarnings("unchecked")
    List<Map<String, Object>> steps = (List<Map<String, Object>>) workflow.get("steps");

    if (state.workflowId != null && !state.workflowId.isEmpty()) {
      System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
      api("POST", "/workflows/" + state.workflowId, Map.of("steps", steps));
    } else {
      try {
        String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
        Map<String, Object> list = api("GET", "/workflows?name=" + encodedName, null);

        @SuppressWarnings("unchecked")
        List<Map<String, Object>> items = (List<Map<String, Object>>) (list.get("data") != null ? list.get("data") : list.get("items"));
        if (items == null) items = List.of();

        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");
              api("POST", "/workflows/" + existingId, Map.of("steps", steps));
              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) {
          @SuppressWarnings("unchecked")
          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 + ")");
      }
    }

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

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

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

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

  private static Map<String, Object> api(String method, String pathName, Map<String, Object> body) throws Exception {
    HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
        .uri(URI.create(API + pathName))
        .header("Authorization", "Bearer " + API_KEY)
        .header("x-extend-api-version", VERSION);

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

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

    Map<String, Object> data = new LinkedHashMap<>();
    if (!response.body().isEmpty()) {
      data = parseJson(response.body());
    }

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

    return data;
  }

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

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

    return workflow;
  }

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

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

    Map<String, Object> parseConfig = new LinkedHashMap<>();
    Map<String, Object> blockOptions = new LinkedHashMap<>();
    Map<String, Object> textOptions = new LinkedHashMap<>();
    Map<String, Object> agenticOptions = new LinkedHashMap<>();
    agenticOptions.put("enabled", true);
    textOptions.put("agentic", agenticOptions);
    blockOptions.put("text", textOptions);
    parseConfig.put("blockOptions", blockOptions);

    Map<String, Object> chunkingStrategy = new LinkedHashMap<>();
    chunkingStrategy.put("type", "document");
    parseConfig.put("chunkingStrategy", chunkingStrategy);

    Map<String, Object> config = new LinkedHashMap<>();
    config.put("parseConfig", parseConfig);
    step.put("config", config);

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

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

    Map<String, Object> extractorConfig = new LinkedHashMap<>();
    extractorConfig.put("schema", buildSchema());
    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);

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

    return step;
  }

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

    Map<String, Object> properties = new LinkedHashMap<>();
    properties.put("notes", buildStringProperty("Any additional comments, instructions, or messages included on the invoice. May include thank you notes, payment instructions, or other relevant information."));
    properties.put("due_date", buildDateProperty("The date by which payment for this invoice is expected. May be explicitly stated or derived from payment terms. Look for labels like 'Due Date', 'Payment Due', or similar."));
    properties.put("line_items", buildLineItemsProperty());
    properties.put("tax_amount", buildCurrencyProperty("The total amount of tax applied to this invoice. May include sales tax, VAT, or other applicable taxes. Look for labels such as 'Tax', 'VAT', or similar."));
    properties.put("vendor_name", buildStringProperty("The name of the company or entity issuing this invoice and requesting payment. This is the billing party or service provider. May appear under headings such as 'From', 'Seller', or 'Vendor'."));
    properties.put("invoice_date", buildDateProperty("The date when this invoice or bill was created or issued. This is the official date of the document, used for accounting and payment term calculations. May appear with labels such as 'Date', 'Invoice Date', or similar."));
    properties.put("total_amount", buildCurrencyProperty("The final amount owed by the customer, including all charges, taxes, and adjustments. This is the complete payment obligation. May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar."));
    properties.put("customer_name", buildStringProperty("The name of the customer, client, or company being billed. This is the recipient of the invoice. May appear under headings such as 'Bill To', 'Customer', or 'Client'."));
    properties.put("payment_terms", buildStringProperty("The terms specifying when payment is due and any conditions for payment. May include phrases like 'Net 30', 'Due on receipt', or other arrangements. Look for sections labeled 'Payment Terms', 'Terms', or similar."));
    properties.put("invoice_number", buildStringProperty("The unique identifier assigned to this invoice or billing document. This is the primary reference number for the transaction and may include numbers, letters, or special characters. Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely."));
    properties.put("vendor_address", buildStringProperty("The full mailing address of the vendor or service provider issuing the invoice. This may include street address, city, state, postal code, and country. Typically found near the vendor name."));
    properties.put("vendor_contact", buildStringProperty("Contact information for the vendor, such as phone number or email address. May appear near the vendor name or address, and can include multiple contact methods."));
    properties.put("subtotal_amount", buildCurrencyProperty("The sum of all line item charges before taxes, discounts, or additional fees are applied. This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar."));
    properties.put("customer_address", buildStringProperty("The full mailing address of the customer or recipient of the invoice. This may include street address, city, state, postal code, and country. Typically found near the customer name."));

    schema.put("properties", properties);

    List<String> required = List.of(
        "notes", "due_date", "line_items", "tax_amount", "vendor_name", "invoice_date",
        "total_amount", "customer_name", "payment_terms", "invoice_number", "vendor_address",
        "vendor_contact", "subtotal_amount", "customer_address"
    );
    schema.put("required", required);
    schema.put("additionalProperties", false);

    return schema;
  }

  private static Map<String, Object> buildStringProperty(String description) {
    Map<String, Object> prop = new LinkedHashMap<>();
    prop.put("type", List.of("string", "null"));
    prop.put("description", description);
    return prop;
  }

  private static Map<String, Object> buildDateProperty(String description) {
    Map<String, Object> prop = new LinkedHashMap<>();
    prop.put("type", List.of("string", "null"));
    prop.put("extend:type", "date");
    prop.put("description", description);
    return prop;
  }

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

    Map<String, Object> currencyProps = new LinkedHashMap<>();
    Map<String, Object> amountProp = new LinkedHashMap<>();
    amountProp.put("type", List.of("number", "null"));
    currencyProps.put("amount", amountProp);

    Map<String, Object> currencyCodeProp = new LinkedHashMap<>();
    currencyCodeProp.put("type", List.of("string", "null"));
    currencyProps.put("iso_4217_currency_code", currencyCodeProp);

    prop.put("properties", currencyProps);
    prop.put("additionalProperties", false);
    prop.put("required", List.of("amount", "iso_4217_currency_code"));
    prop.put("extend:type", "currency");
    prop.put("description", description);

    return prop;
  }

  private static Map<String, Object> buildLineItemsProperty() {
    Map<String, Object> prop = new LinkedHashMap<>();
    prop.put("type", "array");

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

    Map<String, Object> itemProps = new LinkedHashMap<>();
    Map<String, Object> totalProp = new LinkedHashMap<>();
    totalProp.put("type", List.of("number", "null"));
    totalProp.put("description", "The total cost for this line item, typically calculated as quantity multiplied by unit price. May also include item-level adjustments.");
    itemProps.put("total", totalProp);

    Map<String, Object> quantityProp = new LinkedHashMap<>();
    quantityProp.put("type", List.of("number", "null"));
    quantityProp.put("description", "The number of units, hours, or quantity for this line item. Can be a whole number or decimal, depending on the item.");
    itemProps.put("quantity", quantityProp);

    Map<String, Object> unitPriceProp = new LinkedHashMap<>();
    unitPriceProp.put("type", List.of("number", "null"));
    unitPriceProp.put("description", "The price per single unit of this item before quantity multiplication. This is the base rate or price for one unit.");
    itemProps.put("unit_price", unitPriceProp);

    Map<String, Object> descriptionProp = new LinkedHashMap<>();
    descriptionProp.put("type", List.of("string", "null"));
    descriptionProp.put("description", "A description of the product, service, or charge for this line item. May include item names, service descriptions, or product codes.");
    itemProps.put("description", descriptionProp);

    itemSchema.put("properties", itemProps);
    itemSchema.put("additionalProperties", false);
    itemSchema.put("required", List.of("total", "quantity", "unit_price", "description"));

    prop.put("items", itemSchema);
    prop.put("description", "The individual products, services, or charges that make up this invoice. Each item typically includes a description, quantity, unit price, and total. Format may vary from tables to lists or other structures.");

    return prop;
  }

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

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

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

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

    while (pos[0] < json.length()) {
      skipWhitespace(json, pos);
      if (pos[0] >= json.length()) break;
      if (json.charAt(pos[0]) == '}') {
        pos[0]++;
        break;
      }

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

      skipWhitespace(json, pos);
      if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
        pos[0]++;
      }
    }
    return map;
  }

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

    while (pos[0] < json.length()) {
      skipWhitespace(json, pos);
      if (pos[0] >= json.length()) break;
      if (json.charAt(pos[0]) == ']') {
        pos[0]++;
        break;
      }

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

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

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

  private static String parseJsonString(String json, int[] pos) {
    skipWhitespace(json, pos);
    if (pos[0] >= json.length() || json.charAt(pos[0]) != '"') return "";
    pos[0]++;

    StringBuilder sb = new StringBuilder();
    while (pos[0] < json.length()) {
      char c = json.charAt(pos[0]);
      if (c == '"') {
        pos[0]++;
        break;
      }
      if (c == '\\' && pos[0] + 1 < json.length()) {
        pos[0]++;
        char escaped = json.charAt(pos[0]);
        switch (escaped) {
          case '"': sb.append('"'); break;
          case '\\': sb.append('\\'); break;
          case '/': sb.append('/'); break;
          case 'b': sb.append('\b'); break;
          case 'f': sb.append('\f'); break;
          case 'n': sb.append('\n'); break;
          case 'r': sb.append('\r'); break;
          case 't': sb.append('\t'); break;
          default: sb.append(escaped);
        }
      } else {
        sb.append(c);
      }
      pos[0]++;
    }
    return sb.toString();
  }

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

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

package main

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

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

var (
	apiKey   string
	stateDir string
	stateFile string
)

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

var state State

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

	cwd, err := os.Getwd()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
		os.Exit(1)
	}

	stateDir = filepath.Join(cwd, ".extend")
	stateFile = filepath.Join(stateDir, "invoice-extractor.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 reqBody io.Reader
	if body != nil {
		data, err := json.Marshal(body)
		if err != nil {
			return nil, err
		}
		reqBody = bytes.NewReader(data)
	}

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

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

	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
}

var workflow = map[string]interface{}{
	"name": "Invoice Extractor 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{}{
							"notes": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Any additional comments, instructions, or messages included on the invoice. May include thank you notes, payment instructions, or other relevant information.",
							},
							"due_date": map[string]interface{}{
								"type":              []string{"string", "null"},
								"extend:type":       "date",
								"description":       "The date by which payment for this invoice is expected. May be explicitly stated or derived from payment terms. Look for labels like 'Due Date', 'Payment Due', or similar.",
							},
							"line_items": map[string]interface{}{
								"type": "array",
								"items": map[string]interface{}{
									"type": "object",
									"properties": map[string]interface{}{
										"total": map[string]interface{}{
											"type":        []string{"number", "null"},
											"description": "The total cost for this line item, typically calculated as quantity multiplied by unit price. May also include item-level adjustments.",
										},
										"quantity": map[string]interface{}{
											"type":        []string{"number", "null"},
											"description": "The number of units, hours, or quantity for this line item. Can be a whole number or decimal, depending on the item.",
										},
										"unit_price": map[string]interface{}{
											"type":        []string{"number", "null"},
											"description": "The price per single unit of this item before quantity multiplication. This is the base rate or price for one unit.",
										},
										"description": map[string]interface{}{
											"type":        []string{"string", "null"},
											"description": "A description of the product, service, or charge for this line item. May include item names, service descriptions, or product codes.",
										},
									},
									"additionalProperties": false,
									"required":             []string{"total", "quantity", "unit_price", "description"},
								},
								"description": "The individual products, services, or charges that make up this invoice. Each item typically includes a description, quantity, unit price, and total. Format may vary from tables to lists or other structures.",
							},
							"tax_amount": map[string]interface{}{
								"type": "object",
								"properties": map[string]interface{}{
									"amount": map[string]interface{}{
										"type": []string{"number", "null"},
									},
									"iso_4217_currency_code": map[string]interface{}{
										"type": []string{"string", "null"},
									},
								},
								"additionalProperties": false,
								"required":             []string{"amount", "iso_4217_currency_code"},
								"extend:type":          "currency",
								"description":          "The total amount of tax applied to this invoice. May include sales tax, VAT, or other applicable taxes. Look for labels such as 'Tax', 'VAT', or similar.",
							},
							"vendor_name": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The name of the company or entity issuing this invoice and requesting payment. This is the billing party or service provider. May appear under headings such as 'From', 'Seller', or 'Vendor'.",
							},
							"invoice_date": map[string]interface{}{
								"type":        []string{"string", "null"},
								"extend:type": "date",
								"description": "The date when this invoice or bill was created or issued. This is the official date of the document, used for accounting and payment term calculations. May appear with labels such as 'Date', 'Invoice Date', or similar.",
							},
							"total_amount": map[string]interface{}{
								"type": "object",
								"properties": map[string]interface{}{
									"amount": map[string]interface{}{
										"type": []string{"number", "null"},
									},
									"iso_4217_currency_code": map[string]interface{}{
										"type": []string{"string", "null"},
									},
								},
								"additionalProperties": false,
								"required":             []string{"amount", "iso_4217_currency_code"},
								"extend:type":          "currency",
								"description":          "The final amount owed by the customer, including all charges, taxes, and adjustments. This is the complete payment obligation. May appear with labels such as 'Total', 'Amount Due', 'Balance', or similar.",
							},
							"customer_name": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The name of the customer, client, or company being billed. This is the recipient of the invoice. May appear under headings such as 'Bill To', 'Customer', or 'Client'.",
							},
							"payment_terms": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The terms specifying when payment is due and any conditions for payment. May include phrases like 'Net 30', 'Due on receipt', or other arrangements. Look for sections labeled 'Payment Terms', 'Terms', or similar.",
							},
							"invoice_number": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The unique identifier assigned to this invoice or billing document. This is the primary reference number for the transaction and may include numbers, letters, or special characters. Common labels include 'Invoice #', 'Bill Number', or 'Reference', but terminology and placement can vary widely.",
							},
							"vendor_address": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The full mailing address of the vendor or service provider issuing the invoice. This may include street address, city, state, postal code, and country. Typically found near the vendor name.",
							},
							"vendor_contact": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "Contact information for the vendor, such as phone number or email address. May appear near the vendor name or address, and can include multiple contact methods.",
							},
							"subtotal_amount": map[string]interface{}{
								"type": "object",
								"properties": map[string]interface{}{
									"amount": map[string]interface{}{
										"type": []string{"number", "null"},
									},
									"iso_4217_currency_code": map[string]interface{}{
										"type": []string{"string", "null"},
									},
								},
								"additionalProperties": false,
								"required":             []string{"amount", "iso_4217_currency_code"},
								"extend:type":          "currency",
								"description":          "The sum of all line item charges before taxes, discounts, or additional fees are applied. This is the pre-tax, pre-adjustment total. May be labeled as 'Subtotal' or similar.",
							},
							"customer_address": map[string]interface{}{
								"type":        []string{"string", "null"},
								"description": "The full mailing address of the customer or recipient of the invoice. This may include street address, city, state, postal code, and country. Typically found near the customer name.",
							},
						},
						"required": []string{
							"notes", "due_date", "line_items", "tax_amount", "vendor_name",
							"invoice_date", "total_amount", "customer_name", "payment_terms",
							"invoice_number", "vendor_address", "vendor_contact", "subtotal_amount",
							"customer_address",
						},
						"additionalProperties": false,
					},
					"baseProcessor": "extraction_performance",
					"advancedOptions": map[string]interface{}{
						"reviewAgent": map[string]interface{}{
							"enabled": true,
						},
						"advancedMultimodalEnabled": true,
					},
				},
			},
		},
	},
}

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

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

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

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

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

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

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

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

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

Frequently Asked Questions (FAQ)

Set a confidence threshold in your pipeline (e.g., route extractions with `confidence < 0.8` to human review), and use `baseProcessor: "extraction_performance"` instead of `extraction_light` to improve accuracy. Also refine your schema field descriptions — specificity (e.g., 'total amount due including tax, e.g. $1,234.56') directly boosts confidence.
Nuanced question and depends on the use case! For an agent pipeline, you'll likely just stop at Parsing, take the markdown/HTML output and feed that into your pipeline. For Key-Value extraction into JSON, you can jump straight into Extraction because there is always a Parse step beforehand
Tags
Food DistributionWholesaleInvoice ProcessingDelivery OrdersSupply Chain
About this template

This template processes invoices, capturing customer information, delivery dates, itemized products with quantities and pricing, tax amounts, and invoice adjustments. It handles multi-line item descriptions and complex pricing structures.

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

Relevant templates for Financial & Banking

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