Extracts itemized sales, pricing, GST tax, and payment details from retail receipts.
A point-of-sale receipt is a transactional document issued by a retail merchant that records the itemized products or services purchased, quantities, prices, applicable taxes, payment method, and change due. This template takes in Receipts and outputs markdown (.md) capturing the receipt's full text layout and merchant details, and JSON (.json) with structured extraction fields including line items, vendor information, tax calculations, payment amounts, and transaction metadata per the schema by using Extend's Parse, Extract primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.barcodes.readingEnabledfalsechangedblockOptions.figures.enabledfalsechangedblockOptions.formulas.enabledfalsechangedblockOptions.tables.targetFormat"markdown"changedblockOptions.text.signatureDetectionEnabledfalsechangedengine"parse_light"changedblockOptions.text.agentic.enabledfalsechunkingStrategy.type"document"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
schemacustom schema — 13 fieldschangedbaseProcessor"extraction_light"changedextractionRulesno custom rulesadvancedOptions.advancedMultimodalEnabledfalseadvancedOptions.reviewAgent.enabledfalseYou can learn more about Extract configuration in Extend's Extract documentation.
{
"name": "Receipt Parse + Extract Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": false
},
"signatureDetectionEnabled": false
},
"tables": {
"targetFormat": "markdown"
},
"figures": {
"enabled": false
},
"barcodes": {
"readingEnabled": false
},
"formulas": {
"enabled": false
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"required": [
"line_items",
"tax_amount",
"vendor_name",
"total_amount",
"vendor_email",
"vendor_phone",
"change_amount",
"customer_name",
"payment_method",
"receipt_number",
"vendor_address",
"subtotal_amount",
"transaction_date"
],
"properties": {
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"quantity",
"unit_price",
"description",
"total_price"
],
"properties": {
"quantity": {
"type": [
"number",
"null"
],
"description": "The number of units purchased for this line item. May 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 any quantity multiplication or discounts."
},
"description": {
"type": [
"string",
"null"
],
"description": "A description of the product or service purchased in this line item. May include item name, SKU, or other identifying details."
},
"total_price": {
"type": [
"number",
"null"
],
"description": "The total price for this line item, typically calculated as quantity multiplied by unit price, before taxes or discounts."
}
},
"additionalProperties": false
},
"description": "The individual products or services purchased in this transaction. Each item typically includes a description, quantity, unit price, and total price. Formats vary widely, 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 tax charged for this transaction. May be labeled as 'Tax', 'Sales Tax', 'VAT', or similar. If multiple taxes are present, this should be the combined total.",
"extend:type": "currency",
"additionalProperties": false
},
"vendor_name": {
"type": [
"string",
"null"
],
"description": "The name of the business, merchant, or entity that issued this receipt and received payment. This is the party providing goods or services."
},
"total_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 paid for this transaction, including all items, taxes, fees, and adjustments. This is the final payment amount and may be labeled as 'Total', 'Amount Paid', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"vendor_email": {
"type": [
"string",
"null"
],
"description": "The email address of the business or merchant issuing the receipt, if present."
},
"vendor_phone": {
"type": [
"string",
"null"
],
"description": "The phone number of the business or merchant issuing the receipt. May include country and area codes."
},
"change_amount": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The amount of change returned to the customer, if payment exceeded the total amount. May be labeled as 'Change', 'Cash Back', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"customer_name": {
"type": [
"string",
"null"
],
"description": "The name of the customer or purchaser, if specified on the receipt. May be labeled as 'Customer', 'Sold To', or similar."
},
"payment_method": {
"type": [
"string",
"null"
],
"description": "The method of payment used for this transaction, such as 'Credit Card', 'Cash', 'Debit', 'Mobile Payment', or specific card type. May include partial card numbers or other identifiers."
},
"receipt_number": {
"type": [
"string",
"null"
],
"description": "The unique identifier or reference number for this receipt. This may include numbers, letters, or special characters. Commonly labeled as 'Receipt #', 'Transaction ID', or similar, but the key is identifying the primary reference for this transaction."
},
"vendor_address": {
"type": [
"string",
"null"
],
"description": "The address of the business or merchant issuing the receipt. May include street, city, state, postal code, and country. Sometimes appears as a block of text or in multiple lines."
},
"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 item prices before taxes, discounts, or additional fees. May be labeled as 'Subtotal', 'Items Total', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"transaction_date": {
"type": [
"string",
"null"
],
"description": "The date when the transaction occurred and the receipt was issued. This is the official date for accounting and record-keeping purposes. May be labeled as 'Date', 'Transaction Date', or similar.",
"extend:type": "date"
}
},
"additionalProperties": false
},
"baseProcessor": "extraction_light",
"advancedOptions": {
"reviewAgent": {
"enabled": false
},
"advancedMultimodalEnabled": false
}
}
}
}
]
}# Receipt Parse + Extract — Extend AI Skill
## What this pipeline does
This pipeline converts retail point-of-sale receipts and tax invoices into structured, machine-readable data. It first parses the receipt to markdown (preserving tables of line items), then extracts 13 key fields including merchant details, itemized purchases with quantities and prices, GST/tax calculations, payment method, and change due. Output is JSON suitable for accounting systems, expense management platforms, and tax compliance workflows in Malaysia and similar markets.
## When to use this
- **Expense tracking & reimbursement**: Automatically capture receipt data for employee expense reports without manual data entry.
- **Retail POS integration**: Batch-process end-of-day receipts to feed accounting software (QuickBooks, Xero, SAP) with line-item and tax detail.
- **Tax compliance**: Extract GST amounts and transaction dates for Malaysian GST filing and audit trails.
- **Inventory reconciliation**: Capture item descriptions, quantities, and unit prices to cross-check against purchase orders.
- **Customer analytics**: Extract customer names and payment methods to build transaction history and spending patterns.
## Processor pipeline
### Step 1: Parse (extraction_light mode)
**Purpose**: Convert receipt image/PDF to markdown, preserving table structure of line items.
**Config chosen**:
- `engine: "parse_light"` — receipts are typically short, clean digital documents; light mode is 3–5× faster than agentic OCR and sufficient for non-handwritten POS output.
- `blockOptions.tables.targetFormat: "markdown"` — converts itemized tables to pipe-delimited markdown, which preserves column alignment and is easier for the extraction step to parse.
- `blockOptions.text.agentic.enabled: false` — receipts rarely have complex layouts; disabling agentic mode saves cost and latency.
- `blockOptions.figures.enabled: false`, `barcodes.readingEnabled: false` — receipts don't require barcode or logo extraction; disable to reduce processing time.
- `chunkingStrategy.type: "document"` — receipts are typically < 1 page; return one chunk per document rather than breaking into overlapping windows.
**Why**: Receipts are short, structured documents. Aggressive optimization (no agentic text, no figures, tables as markdown) minimizes latency while preserving the data needed for extraction.
### Step 2: Extract (extraction_light + full schema)
**Purpose**: Pull 13 fields from the parsed markdown into a typed JSON object: vendor info, line items with qty/price, totals, tax, payment method, change, receipt number, transaction date, and customer name.
**Config chosen**:
- `baseProcessor: "extraction_light"` — matches parse speed tier; adequate for structured POS data with clear field labels.
- Schema includes 3 currency fields (`tax_amount`, `subtotal_amount`, `total_amount`, `change_amount`) with ISO 4217 code support for multi-currency handling.
- `line_items` array with `required: ["quantity", "unit_price", "description", "total_price"]` — captures full itemization for audit and reconciliation.
- All top-level fields except `line_items` are nullable (`type: ["string", "null"]` or `["number", "null"]`) because receipts vary widely: some may lack vendor email, customer name, or change amount.
**Why**: Extraction_light is tuned for receipts: consistent field order, clear labels, and tabular line items. Currency objects (with amount + currency code) ensure data is usable globally.
---
## TypeScript implementation
---
## CLI equivalent
```bash
# Step 1: Parse receipt to markdown
extend parse receipt.pdf \
--config '{
"mode": "light",
"blockOptions": {
"text": { "agentic": { "enabled": false }, "signatureDetectionEnabled": false },
"figures": { "enabled": false },
"tables": { "targetFormat": "markdown" },
"formulas": { "enabled": false },
"barcodes": { "readingEnabled": false }
},
"chunkingStrategy": { "type": "document" }
}'
# Step 2: Extract structured fields
extend extract receipt.pdf \
--schema schema.json \
--processor extraction_light
```
**schema.json** (save this file):
```json
{
"type": "object",
"properties": {
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"quantity": { "type": ["number", "null"], "description": "Number of units purchased" },
"unit_price": { "type": ["number", "null"], "description": "Price per single unit before quantity/discounts" },
"description": { "type": ["string", "null"], "description": "Product or service name and details" },
"total_price": { "type": ["number", "null"], "description": "Total for this line (qty × unit_price), pre-tax" }
},
"required": ["quantity", "unit_price", "description", "total_price"]
},
"description": "Individual products/services in transaction with quantities, prices, and totals"
},
"tax_amount": {
"type": "object",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"],
"description": "Total tax charged (GST, Sales Tax, VAT, or combined)"
},
"vendor_name": { "type": ["string", "null"], "description": "Legal business/merchant name" },
"total_amount": {
"type": "object",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"],
"description": "Final amount paid including items, taxes, fees"
},
"vendor_email": { "type": ["string", "null"], "description": "Email address of merchant" },
"vendor_phone": { "type": ["string", "null"], "description": "Phone number of merchant" },
"change_amount": {
"type": "object",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"],
"description": "Amount returned if payment exceeded total (cash)"
},
"customer_name": { "type": ["string", "null"], "description": "Name of customer/purchaser" },
"payment_method": { "type": ["string", "null"], "description": "Payment type: Cash, Credit Card, Debit, Mobile Payment, etc." },
"receipt_number": { "type": ["string", "null"], "description": "Unique transaction/receipt ID" },
"vendor_address": { "type": ["string", "null"], "description": "Full address of business/merchant" },
"subtotal_amount": {
"type": "object",
"properties": {
"amount": { "type": ["number", "null"] },
"iso_4217_currency_code": { "type": ["string", "null"] }
},
"required": ["amount", "iso_4217_currency_code"],
"description": "Sum of all item prices before taxes/discounts"
},
"transaction_date": { "type": ["string", "null"], "description": "Date receipt issued (ISO yyyy-mm-dd)" }
},
"required": ["line_items", "tax_amount", "vendor_name", "total_amount", "vendor_email", "vendor_phone", "change_amount", "customer_name", "payment_method", "receipt_number", "vendor_address", "subtotal_amount", "transaction_date"],
"additionalProperties": false
}
```
---
## Schema
The extraction schema is a flat object with 13 required fields:
| Field | Type | Description | Accuracy Notes |
|-------|------|-------------|-----------------|
| **vendor_name** | string \| null | Legal business name | Critical for audit trail; often at top of receipt or near contact details |
| **vendor_email** | string \| null | Merchant email | May be absent on older/cash-only receipts; use null if not found |
| **vendor_phone** | string \| null | Merchant phone | Often includes country code or extension; capture as-is |
| **vendor_address** | string \| null | Full address block | May span multiple lines; normalize whitespace but preserve city/country |
| **receipt_number** | string \| null | Transaction ID / Reference | Primary key for matching to POS system; never null in healthy receipt |
| **transaction_date** | string (ISO date) | Receipt issue date | **Critical for tax filing**; must be yyyy-mm-dd; use OCR carefully to avoid 12/31 → 31/12 errors |
| **customer_name** | string \| null | Purchaser name | Often null for retail; present on business invoices or loyalty card transactions |
| **payment_method** | string \| null | Cash / Credit Card / Debit / etc. | Categorize into consistent values (e.g. "Cash", "Visa", "GCash") for analytics |
| **subtotal_amount** | currency object | Pre-tax total | **Must match sum of line_items**; validate in post-processing |
| **tax_amount** | currency object | GST / Sales Tax / VAT | **Critical for compliance**; always includes `iso_4217_import { ExtendClient, extendDate, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
export async function processReceiptParseExtract(filePath: string) {
// Step 0: Read local file and convert to base64 data URL
// (Extend SDK does not accept Node.js ReadStreams; use data URL for local files)
const fileBuffer = fs.readFileSync(filePath);
const base64 = fileBuffer.toString("base64");
const mimeType = filePath.endsWith(".pdf") ? "application/pdf" : "image/jpeg";
const dataUrl = `data:${mimeType};base64,${base64}`;
console.log(`Processing receipt: ${filePath}`);
// Step 1: Parse receipt to markdown + bounding boxes
// Preserve table structure for line items; strip unnecessary elements (figures, barcodes, signatures).
console.log("Step 1: Parsing receipt...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
mode: "light",
blockOptions: {
text: {
agentic: { enabled: false },
signatureDetectionEnabled: false,
},
figures: { enabled: false },
tables: { targetFormat: "markdown" },
formulas: { enabled: false },
barcodes: { readingEnabled: false },
},
chunkingStrategy: { type: "document" },
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
const parsedMarkdown = parseRun.output.chunks
.map((c) => c.content)
.join("\n\n");
console.log(`Parsed markdown (first 500 chars):\n${parsedMarkdown.substring(0, 500)}`);
// Step 2: Extract structured fields using Zod schema
// Matches the workflow config schema exactly: 13 fields including line items, totals, tax, merchant info.
console.log("\nStep 2: Extracting fields...");
const receiptSchema = z.object({
line_items: z
.array(
z.object({
quantity: z.number().nullable().describe("Number of units purchased"),
unit_price: z.number().nullable().describe("Price per single unit before quantity/discounts"),
description: z.string().nullable().describe("Product or service name and details"),
total_price: z.number().nullable().describe("Total for this line (qty × unit_price), pre-tax"),
})
)
.describe(
"Individual products/services in this transaction with quantities, prices, and totals. Formats vary: tables, lists, or mixed."
),
tax_amount: extendCurrency().describe(
"Total tax charged (GST, Sales Tax, VAT, or combined if multiple). Labeled as 'Tax', 'GST', 'Sales Tax', etc."
),
vendor_name: z.string().nullable().describe("Legal business/merchant name that issued receipt and received payment"),
total_amount: extendCurrency().describe("Final amount paid including all items, taxes, fees, and adjustments"),
vendor_email: z.string().nullable().describe("Email address of business/merchant issuing receipt, if present"),
vendor_phone: z.string().nullable().describe("Phone number of business/merchant, may include country/area codes"),
change_amount: extendCurrency().describe(
"Amount returned if payment exceeded total (cash transactions). May be labeled 'Change' or 'Cash Back'."
),
customer_name: z.string().nullable().describe("Name of customer/purchaser if specified. May be labeled 'Customer', 'Sold To'"),
payment_method: z
.string()
.nullable()
.describe(
"Payment type: 'Cash', 'Credit Card', 'Debit', 'Mobile Payment', etc. May include partial card numbers or identifiers."
),
receipt_number: z
.string()
.nullable()
.describe("Unique transaction/receipt ID (alphanumeric). Labeled 'Receipt #', 'Transaction ID', 'Ref #', etc."),
vendor_address: z.string().nullable().describe("Full address of business/merchant (street, city, postal code, country)"),
subtotal_amount: extendCurrency().describe("Sum of all item prices before taxes, discounts, or fees"),
transaction_date: extendDate().describe("Date receipt was issued (ISO yyyy-mm-dd format for accounting/tax purposes)"),
});
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: receiptSchema,
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed with status: ${extractRun.status}`);
}
const result = extractRun.output.value;
// Step 3: Output and validate
console.log("\n=== EXTRACTION RESULT ===");
console.log(JSON.stringify(result, null, 2));
// Example: Print key summaries for quick review
console.log("\n=== SUMMARY ===");
console.log(`Vendor: ${result.vendor_name}`);
console.log(`Receipt #: ${result.receipt_number}`);
console.log(`Date: ${result.transaction_date}`);
console.log(`Line items: ${result.line_items.length}`);
console.log(`Subtotal: ${result.subtotal_amount?.amount} ${result.subtotal_amount?.iso_4217_currency_code}`);
console.log(`Tax: ${result.tax_amount?.amount} ${result.tax_amount?.iso_4217_currency_code}`);
console.log(`Total: ${result.total_amount?.amount} ${result.total_amount?.iso_4217_currency_code}`);
if (result.change_amount?.amount) {
console.log(`Change: ${result.change_amount.amount} ${result.change_amount.iso_4217_currency_code}`);
}
console.log(`Payment: ${result.payment_method}`);
return result;
}
// Auto-invoke if run directly
if (require.main === module) {
const filePath = process.argv[2] || "./receipt.pdf";
processReceiptParseExtract(filePath).catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});
}import os
import base64
from extend_ai import Extend
client = Extend(token=os.environ["EXTEND_API_KEY"])
def process_receipt_parse_extract(file_path: str):
# Step 0: Read local file and convert to base64 data URL
# (Extend SDK does not accept Node.js ReadStreams; use data URL for local files)
with open(file_path, "rb") as f:
file_buffer = f.read()
base64_str = base64.b64encode(file_buffer).decode("utf-8")
mime_type = "application/pdf" if file_path.endswith(".pdf") else "image/jpeg"
data_url = f"data:{mime_type};base64,{base64_str}"
print(f"Processing receipt: {file_path}")
# Step 1: Parse receipt to markdown + bounding boxes
# Preserve table structure for line items; strip unnecessary elements (figures, barcodes, signatures).
print("Step 1: Parsing receipt...")
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"mode": "light",
"block_options": {
"text": {
"agentic": {"enabled": False},
"signature_detection_enabled": False,
},
"figures": {"enabled": False},
"tables": {"target_format": "markdown"},
"formulas": {"enabled": False},
"barcodes": {"reading_enabled": False},
},
"chunking_strategy": {"type": "document"},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
parsed_markdown = "\n\n".join([c.content for c in parse_run.output.chunks])
print(f"Parsed markdown (first 500 chars):\n{parsed_markdown[:500]}")
# Step 2: Extract structured fields using a plain JSON-schema-style dict
# Matches the workflow config schema exactly: 13 fields including line items, totals, tax, merchant info.
print("\nStep 2: Extracting fields...")
receipt_schema = {
"type": "object",
"properties": {
"line_items": {
"type": "array",
"description": "Individual products/services in this transaction with quantities, prices, and totals. Formats vary: tables, lists, or mixed.",
"items": {
"type": "object",
"properties": {
"quantity": {
"type": ["number", "null"],
"description": "Number of units purchased",
},
"unit_price": {
"type": ["number", "null"],
"description": "Price per single unit before quantity/discounts",
},
"description": {
"type": ["string", "null"],
"description": "Product or service name and details",
},
"total_price": {
"type": ["number", "null"],
"description": "Total for this line (qty × unit_price), pre-tax",
},
},
},
},
"tax_amount": {
"type": "object",
"description": "Total tax charged (GST, Sales Tax, VAT, or combined if multiple). Labeled as 'Tax', 'GST', 'Sales Tax', etc.",
"properties": {
"amount": {"type": "number"},
"iso_4217_currency_code": {"type": "string"},
},
},
"vendor_name": {
"type": ["string", "null"],
"description": "Legal business/merchant name that issued receipt and received payment",
},
"total_amount": {
"type": "object",
"description": "Final amount paid including all items, taxes, fees, and adjustments",
"properties": {
"amount": {"type": "number"},
"iso_4217_currency_code": {"type": "string"},
},
},
"vendor_email": {
"type": ["string", "null"],
"description": "Email address of business/merchant issuing receipt, if present",
},
"vendor_phone": {
"type": ["string", "null"],
"description": "Phone number of business/merchant, may include country/area codes",
},
"change_amount": {
"type": "object",
"description": "Amount returned if payment exceeded total (cash transactions). May be labeled 'Change' or 'Cash Back'.",
"properties": {
"amount": {"type": "number"},
"iso_4217_currency_code": {"type": "string"},
},
},
"customer_name": {
"type": ["string", "null"],
"description": "Name of customer/purchaser if specified. May be labeled 'Customer', 'Sold To'",
},
"payment_method": {
"type": ["string", "null"],
"description": "Payment type: 'Cash', 'Credit Card', 'Debit', 'Mobile Payment', etc. May include partial card numbers or identifiers.",
},
"receipt_number": {
"type": ["string", "null"],
"description": "Unique transaction/receipt ID (alphanumeric). Labeled 'Receipt #', 'Transaction ID', 'Ref #', etc.",
},
"vendor_address": {
"type": ["string", "null"],
"description": "Full address of business/merchant (street, city, postal code, country)",
},
"subtotal_amount": {
"type": "object",
"description": "Sum of all item prices before taxes, discounts, or fees",
"properties": {
"amount": {"type": "number"},
"iso_4217_currency_code": {"type": "string"},
},
},
"transaction_date": {
"type": "string",
"description": "Date receipt was issued (ISO yyyy-mm-dd format for accounting/tax purposes)",
},
},
}
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": receipt_schema,
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed with status: {extract_run.status}")
result = extract_run.output.value
# Step 3: Output and validate
print("\n=== EXTRACTION RESULT ===")
print(result)
# Example: Print key summaries for quick review
print("\n=== SUMMARY ===")
print(f"Vendor: {result.get('vendor_name')}")
print(f"Receipt #: {result.get('receipt_number')}")
print(f"Date: {result.get('transaction_date')}")
print(f"Line items: {len(result.get('line_items', []))}")
subtotal = result.get("subtotal_amount")
if subtotal:
print(
f"Subtotal: {subtotal.get('amount')} {subtotal.get('iso_4217_currency_code')}"
)
tax = result.get("tax_amount")
if tax:
print(f"Tax: {tax.get('amount')} {tax.get('iso_4217_currency_code')}")
total = result.get("total_amount")
if total:
print(f"Total: {total.get('amount')} {total.get('iso_4217_currency_code')}")
change = result.get("change_amount")
if change and change.get("amount"):
print(f"Change: {change.get('amount')} {change.get('iso_4217_currency_code')}")
print(f"Payment: {result.get('payment_method')}")
return result
# Auto-invoke if run directly
if __name__ == "__main__":
import sys
file_path = sys.argv[1] if len(sys.argv) > 1 else "./receipt.pdf"
try:
process_receipt_parse_extract(file_path)
except Exception as err:
print(f"Error: {err}")
sys.exit(1)// Extend has no official Java SDK; this code calls the REST API directly using built-in java.net.http.HttpClient.
// No external dependencies are needed. Ensure EXTEND_API_KEY environment variable is set.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
public class ReceiptParseExtract {
private static final String API_BASE_URL = "https://api.extend.ai";
private static final HttpClient httpClient = HttpClient.newHttpClient();
static class LineItem {
public Double quantity;
public Double unit_price;
public String description;
public Double total_price;
}
static class CurrencyValue {
public Double amount;
public String iso_4217_currency_code;
}
static class ReceiptData {
public List<LineItem> line_items;
public CurrencyValue tax_amount;
public String vendor_name;
public CurrencyValue total_amount;
public String vendor_email;
public String vendor_phone;
public CurrencyValue change_amount;
public String customer_name;
public String payment_method;
public String receipt_number;
public String vendor_address;
public CurrencyValue subtotal_amount;
public String transaction_date;
}
public static ReceiptData processReceiptParseExtract(String filePath) throws Exception {
// Step 0: Read local file and convert to base64 data URL
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
String base64 = Base64.getEncoder().encodeToString(fileBytes);
String mimeType = filePath.endsWith(".pdf") ? "application/pdf" : "image/jpeg";
String dataUrl = "data:" + mimeType + ";base64," + base64;
System.out.println("Processing receipt: " + filePath);
// Step 1: Parse receipt to markdown + bounding boxes
System.out.println("Step 1: Parsing receipt...");
String parseRunId = createParseRun(dataUrl);
Map<String, Object> parseRun = pollParseRun(parseRunId);
if (!"PROCESSED".equals(parseRun.get("status"))) {
throw new Exception("Parse failed with status: " + parseRun.get("status"));
}
String parsedMarkdown = extractMarkdownFromParseRun(parseRun);
System.out.println("Parsed markdown (first 500 chars):\n" +
parsedMarkdown.substring(0, Math.min(500, parsedMarkdown.length())));
// Step 2: Extract structured fields
System.out.println("\nStep 2: Extracting fields...");
String extractRunId = createExtractRun(dataUrl, buildReceiptSchema());
Map<String, Object> extractRun = pollExtractRun(extractRunId);
if (!"PROCESSED".equals(extractRun.get("status"))) {
throw new Exception("Extraction failed with status: " + extractRun.get("status"));
}
ReceiptData result = parseExtractOutput(extractRun);
// Step 3: Output and validate
System.out.println("\n=== EXTRACTION RESULT ===");
printReceiptData(result);
System.out.println("\n=== SUMMARY ===");
System.out.println("Vendor: " + result.vendor_name);
System.out.println("Receipt #: " + result.receipt_number);
System.out.println("Date: " + result.transaction_date);
System.out.println("Line items: " + (result.line_items != null ? result.line_items.size() : 0));
if (result.subtotal_amount != null) {
System.out.println("Subtotal: " + result.subtotal_amount.amount + " " + result.subtotal_amount.iso_4217_currency_code);
}
if (result.tax_amount != null) {
System.out.println("Tax: " + result.tax_amount.amount + " " + result.tax_amount.iso_4217_currency_code);
}
if (result.total_amount != null) {
System.out.println("Total: " + result.total_amount.amount + " " + result.total_amount.iso_4217_currency_code);
}
if (result.change_amount != null && result.change_amount.amount != null) {
System.out.println("Change: " + result.change_amount.amount + " " + result.change_amount.iso_4217_currency_code);
}
System.out.println("Payment: " + result.payment_method);
return result;
}
private static String createParseRun(String dataUrl) throws IOException, InterruptedException {
String body = "{"
+ "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
+ "\"config\":{"
+ "\"mode\":\"light\","
+ "\"blockOptions\":{"
+ "\"text\":{\"agentic\":{\"enabled\":false},\"signatureDetectionEnabled\":false},"
+ "\"figures\":{\"enabled\":false},"
+ "\"tables\":{\"targetFormat\":\"markdown\"},"
+ "\"formulas\":{\"enabled\":false},"
+ "\"barcodes\":{\"readingEnabled\":false}"
+ "},"
+ "\"chunkingStrategy\":{\"type\":\"document\"}"
+ "}"
+ "}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/parse/runs"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + System.getenv("EXTEND_API_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> responseBody = parseJson(response.body());
return (String) responseBody.get("id");
}
private static Map<String, Object> pollParseRun(String runId) throws IOException, InterruptedException, InterruptedException {
for (int i = 0; i < 120; i++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/parse/runs/" + runId))
.header("Authorization", "Bearer " + System.getenv("EXTEND_API_KEY"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> responseBody = parseJson(response.body());
String status = (String) responseBody.get("status");
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
return responseBody;
}
Thread.sleep(1000);
}
throw new RuntimeException("Parse run polling timeout");
}
private static String extractMarkdownFromParseRun(Map<String, Object> parseRun) {
List<Map<String, Object>> chunks = (List<Map<String, Object>>) parseRun.get("chunks");
StringBuilder markdown = new StringBuilder();
for (Map<String, Object> chunk : chunks) {
String content = (String) chunk.get("content");
if (content != null) {
markdown.append(content).append("\n\n");
}
}
return markdown.toString();
}
private static Map<String, Object> buildReceiptSchema() {
Map<String, Object> schema = new HashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new HashMap<>();
// line_items
Map<String, Object> lineItemsSchema = new HashMap<>();
lineItemsSchema.put("type", "array");
Map<String, Object> itemSchema = new HashMap<>();
itemSchema.put("type", "object");
Map<String, Object> itemProps = new HashMap<>();
itemProps.put("quantity", Map.of("type", "number", "description", "Number of units purchased"));
itemProps.put("unit_price", Map.of("type", "number", "description", "Price per single unit"));
itemProps.put("description", Map.of("type", "string", "description", "Product or service name and details"));
itemProps.put("total_price", Map.of("type", "number", "description", "Total for this line"));
itemSchema.put("properties", itemProps);
lineItemsSchema.put("items", itemSchema);
properties.put("line_items", lineItemsSchema);
// tax_amount
properties.put("tax_amount", Map.of("type", "object", "extend:type", "currency"));
properties.put("vendor_name", Map.of("type", "string"));
properties.put("total_amount", Map.of("type", "object", "extend:type", "currency"));
properties.put("vendor_email", Map.of("type", "string"));
properties.put("vendor_phone", Map.of("type", "string"));
properties.put("change_amount", Map.of("type", "object", "extend:type", "currency"));
properties.put("customer_name", Map.of("type", "string"));
properties.put("payment_method", Map.of("type", "string"));
properties.put("receipt_number", Map.of("type", "string"));
properties.put("vendor_address", Map.of("type", "string"));
properties.put("subtotal_amount", Map.of("type", "object", "extend:type", "currency"));
properties.put("transaction_date", Map.of("type", "string", "extend:type", "date"));
schema.put("properties", properties);
return schema;
}
private static String createExtractRun(String dataUrl, Map<String, Object> schema) throws IOException, InterruptedException {
String schemaJson = mapToJson(schema);
String body = "{"
+ "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
+ "\"config\":{\"schema\":" + schemaJson + "}"
+ "}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/extract/runs"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + System.getenv("EXTEND_API_KEY"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> responseBody = parseJson(response.body());
return (String) responseBody.get("id");
}
private static Map<String, Object> pollExtractRun(String runId) throws IOException, InterruptedException {
for (int i = 0; i < 120; i++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE_URL + "/extract/runs/" + runId))
.header("Authorization", "Bearer " + System.getenv("EXTEND_API_KEY"))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> responseBody = parseJson(response.body());
String status = (String) responseBody.get("status");
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
return responseBody;
}
Thread.sleep(1000);
}
throw new RuntimeException("Extract run polling timeout");
}
private static ReceiptData parseExtractOutput(Map<String, Object> extractRun) {
ReceiptData data = new ReceiptData();
Map<String, Object> output = (Map<String, Object>) extractRun.get("output");
Map<String, Object> value = (Map<String, Object>) output.get("value");
// Parse line_items
List<Map<String, Object>> rawLineItems = (List<Map<String, Object>>) value.get("line_items");
data.line_items = new ArrayList<>();
if (rawLineItems != null) {
for (Map<String, Object> item : rawLineItems) {
LineItem li = new LineItem();
li.quantity = toDouble(item.get("quantity"));
li.unit_price = toDouble(item.get("unit_price"));
li.description = (String) item.get("description");
li.total_price = toDouble(item.get("total_price"));
data.line_items.add(li);
}
}
// Parse currency/date fields
data.tax_amount = parseCurrency((Map<String, Object>) value.get("tax_amount"));
data.total_amount = parseCurrency((Map<String, Object>) value.get("total_amount"));
data.change_amount = parseCurrency((Map<String, Object>) value.get("change_amount"));
data.subtotal_amount = parseCurrency((Map<String, Object>) value.get("subtotal_amount"));
// Parse string fields
data.vendor_name = (String) value.get("vendor_name");
data.vendor_email = (String) value.get("vendor_email");
data.vendor_phone = (String) value.get("vendor_phone");
data.customer_name = (String) value.get("customer_name");
data.payment_method = (String) value.get("payment_method");
data.receipt_number = (String) value.get("receipt_number");
data.vendor_address = (String) value.get("vendor_address");
data.transaction_date = (String) value.get("transaction_date");
return data;
}
private static CurrencyValue parseCurrency(Map<String, Object> obj) {
if (obj == null) return null;
CurrencyValue cv = new CurrencyValue();
cv.amount = toDouble(obj.get("amount"));
cv.iso_4217_currency_code = (String) obj.get("iso_4217_currency_code");
return cv;
}
private static Double toDouble(Object obj) {
if (obj == null) return null;
if (obj instanceof Number) return ((Number) obj).doubleValue();
return null;
}
private static void printReceiptData(ReceiptData data) {
System.out.println("{");
System.out.println(" \"line_items\": " + (data.line_items != null ? data.line_items.size() : 0) + " items");
System.out.println(" \"vendor_name\": \"" + data.vendor_name + "\"");
System.out.println(" \"receipt_number\": \"" + data.receipt_number + "\"");
System.out.println(" \"transaction_date\": \"" + data.transaction_date + "\"");
System.out.println(" \"payment_method\": \"" + data.payment_method + "\"");
System.out.println("}");
}
private static String escapeJson(String s) {
return s.replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
private static String mapToJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(entry.getKey()).append("\":");
sb.append(objectToJson(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
private static String objectToJson(Object obj) {
if (obj == null) return "null";
if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
if (obj instanceof Number) return obj.toString();
if (obj instanceof Boolean) return obj.toString();
if (obj instanceof Map) return mapToJson((Map<String, Object>) obj);
if (obj instanceof List) {
List<?> list = (List<?>) obj;
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : list) {
if (!first) sb.append(",");
sb.append(objectToJson(item));
first = false;
}
sb.append("]");
return sb.toString();
}
return "\"" + obj.toString() + "\"";
}
private static Map<String, Object> parseJson(String json) {
// Simple JSON parser for basic objects
Map<String, Object> result = new HashMap<>();
// For production use a real JSON library; this is a minimal implementation
if (json.contains("\"id\"")) {
int idStart = json.indexOf("\"id\":\"") + 6;
int idEnd = json.indexOf("\"", idStart);
result.put("id", json.substring(idStart, idEnd));
}
if (json.contains("\"status\"")) {
int statusStart = json.indexOf("\"status\":\"") + 10;
int statusEnd = json.indexOf("\"", statusStart);
result.put("status", json.substring(statusStart, statusEnd));
}
if (json.contains("\"chunks\"")) {
result.put("chunks", new ArrayList<>());
}
if (json.contains("\"output\"")) {
result.put("output", new HashMap<>());
}
// For a full implementation, use proper JSON parsing
return result;
}
public static void main(String[] args) {
String filePath = args.length > 0 ? args[0] : "./receipt.pdf";
try {
processReceiptParseExtract(filePath);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
System.exit(1);
}
}
}// This code uses Extend's REST API directly because Extend has no official Go SDK yet.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
const extendAPIBase = "https://api.extend.ai"
type LineItem struct {
Quantity *float64 `json:"quantity"`
UnitPrice *float64 `json:"unit_price"`
Description *string `json:"description"`
TotalPrice *float64 `json:"total_price"`
}
type CurrencyAmount struct {
Amount *float64 `json:"amount"`
ISO4217CurrencyCode *string `json:"iso_4217_currency_code"`
}
type ReceiptExtractionResult struct {
LineItems []LineItem `json:"line_items"`
TaxAmount CurrencyAmount `json:"tax_amount"`
VendorName *string `json:"vendor_name"`
TotalAmount CurrencyAmount `json:"total_amount"`
VendorEmail *string `json:"vendor_email"`
VendorPhone *string `json:"vendor_phone"`
ChangeAmount CurrencyAmount `json:"change_amount"`
CustomerName *string `json:"customer_name"`
PaymentMethod *string `json:"payment_method"`
ReceiptNumber *string `json:"receipt_number"`
VendorAddress *string `json:"vendor_address"`
SubtotalAmount CurrencyAmount `json:"subtotal_amount"`
TransactionDate *string `json:"transaction_date"`
}
type SchemaProperty struct {
Type interface{} `json:"type"`
Description string `json:"description,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty"`
Items interface{} `json:"items,omitempty"`
ExtendType string `json:"extend:type,omitempty"`
}
type Schema struct {
Type string `json:"type"`
Properties map[string]SchemaProperty `json:"properties"`
Required []string `json:"required"`
AdditionalProperties bool `json:"additionalProperties"`
}
type ParseConfig struct {
Mode string `json:"mode"`
BlockOptions map[string]interface{} `json:"blockOptions"`
ChunkingStrategy map[string]string `json:"chunkingStrategy"`
}
type ParseRunRequest struct {
File map[string]string `json:"file"`
Config ParseConfig `json:"config"`
}
type ParseRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
} `json:"output"`
}
type ExtractRunRequest struct {
File map[string]string `json:"file"`
Config map[string]interface{} `json:"config"`
}
type ExtractRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output struct {
Value ReceiptExtractionResult `json:"value"`
} `json:"output"`
}
func readFileAsDataURL(filePath string) (string, error) {
fileBuffer, err := os.ReadFile(filePath)
if err != nil {
return "", err
}
base64Str := base64.StdEncoding.EncodeToString(fileBuffer)
mimeType := "image/jpeg"
if strings.HasSuffix(filePath, ".pdf") {
mimeType = "application/pdf"
}
return fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str), nil
}
func createAndPollParseRun(apiKey, dataURL string) (*ParseRunResponse, error) {
parseConfig := ParseConfig{
Mode: "light",
BlockOptions: map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]bool{
"enabled": false,
},
"signatureDetectionEnabled": false,
},
"figures": map[string]interface{}{
"enabled": false,
},
"tables": map[string]string{
"targetFormat": "markdown",
},
"formulas": map[string]interface{}{
"enabled": false,
},
"barcodes": map[string]interface{}{
"readingEnabled": false,
},
},
ChunkingStrategy: map[string]string{
"type": "document",
},
}
req := ParseRunRequest{
File: map[string]string{
"url": dataURL,
},
Config: parseConfig,
}
reqBody, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", extendAPIBase+"/v1/parse-runs/create-and-poll", bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var parseResp ParseRunResponse
err = json.Unmarshal(respBody, &parseResp)
if err != nil {
return nil, err
}
return &parseResp, nil
}
func createAndPollExtractRun(apiKey, dataURL string) (*ExtractRunResponse, error) {
receiptSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"line_items": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"quantity": map[string]interface{}{
"type": []string{"number", "null"},
"description": "The number of units purchased 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 purchased.",
},
"total_price": map[string]interface{}{
"type": []string{"number", "null"},
"description": "The total price for this line item.",
},
},
"additionalProperties": false,
"required": []string{"quantity", "unit_price", "description", "total_price"},
},
"description": "The individual products or services purchased in this transaction.",
},
"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 tax charged for this transaction.",
},
"vendor_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the business that issued this receipt.",
},
"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 total amount paid including all items, taxes, fees, and adjustments.",
},
"vendor_email": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The email address of the business issuing the receipt.",
},
"vendor_phone": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The phone number of the business issuing the receipt.",
},
"change_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 amount of change returned to the customer.",
},
"customer_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the customer or purchaser.",
},
"payment_method": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The method of payment used for this transaction.",
},
"receipt_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The unique identifier for this receipt.",
},
"vendor_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The address of the business issuing the receipt.",
},
"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 item prices before taxes.",
},
"transaction_date": map[string]interface{}{
"type": []string{"string", "null"},
"extend:type": "date",
"description": "The date when the transaction occurred and the receipt was issued.",
},
},
"required": []string{
"line_items", "tax_amount", "vendor_name", "total_amount", "vendor_email",
"vendor_phone", "change_amount", "customer_name", "payment_method",
"receipt_number", "vendor_address", "subtotal_amount", "transaction_date",
},
"additionalProperties": false,
}
req := ExtractRunRequest{
File: map[string]string{
"url": dataURL,
},
Config: map[string]interface{}{
"schema": receiptSchema,
},
}
reqBody, err := json.Marshal(req)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("POST", extendAPIBase+"/v1/extract-runs/create-and-poll", bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
httpReq.Header.Set("Authorization", "Bearer "+os.Getenv("EXTEND_API_KEY"))
httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var extractResp ExtractRunResponse
err = json.Unmarshal(respBody, &extractResp)
if err != nil {
return nil, err
}
return &extractResp, nil
}
func processReceiptParseExtract(filePath string) (*ReceiptExtractionResult, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
dataURL, err := readFileAsDataURL(filePath)
if err != nil {
return nil, err
}
fmt.Printf("Processing receipt: %s\n", filePath)
// Step 1: Parse receipt
fmt.Println("Step 1: Parsing receipt...")
parseRun, err := createAndPollParseRun(apiKey, dataURL)
if err != nil {
return nil, err
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
var parsedMarkdown string
for _, chunk := range parseRun.Output.Chunks {
parsedMarkdown += chunk.Content + "\n\n"
}
if len(parsedMarkdown) > 500 {
fmt.Printf("Parsed markdown (first 500 chars):\n%s\n", parsedMarkdown[:500])
} else {
fmt.Printf("Parsed markdown:\n%s\n", parsedMarkdown)
}
// Step 2: Extract structured fields
fmt.Println("\nStep 2: Extracting fields...")
extractRun, err := createAndPollExtractRun(apiKey, dataURL)
if err != nil {
return nil, err
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extraction failed with status: %s", extractRun.Status)
}
result := &extractRun.Output.Value
// Step 3: Output and validate
fmt.Println("\n=== EXTRACTION RESULT ===")
resultJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(resultJSON))
// Summary
fmt.Println("\n=== SUMMARY ===")
if result.VendorName != nil {
fmt.Printf("Vendor: %s\n", *result.VendorName)
}
if result.ReceiptNumber != nil {
fmt.Printf("Receipt #: %s\n", *result.ReceiptNumber)
}
if result.TransactionDate != nil {
fmt.Printf("Date: %s\n", *result.TransactionDate)
}
fmt.Printf("Line items: %d\n", len(result.LineItems))
if result.SubtotalAmount.Amount != nil && result.SubtotalAmount.ISO4217CurrencyCode != nil {
fmt.Printf("Subtotal: %v %s\n", *result.SubtotalAmount.Amount, *result.SubtotalAmount.ISO4217CurrencyCode)
}
if result.TaxAmount.Amount != nil && result.TaxAmount.ISO4217CurrencyCode != nil {
fmt.Printf("Tax: %v %s\n", *result.TaxAmount.Amount, *result.TaxAmount.ISO4217CurrencyCode)
}
if result.TotalAmount.Amount != nil && result.TotalAmount.ISO4217CurrencyCode != nil {
fmt.Printf("Total: %v %s\n", *result.TotalAmount.Amount, *result.TotalAmount.ISO4217CurrencyCode)
}
if result.ChangeAmount.Amount != nil && *result.ChangeAmount.Amount > 0 {
if result.ChangeAmount.ISO4217CurrencyCode != nil {
fmt.Printf("Change: %v %s\n", *result.ChangeAmount.Amount, *result.ChangeAmount.ISO4217CurrencyCode)
}
}
if result.PaymentMethod != nil {
fmt.Printf("Payment: %s\n", *result.PaymentMethod)
}
return result, nil
}
func main() {
filePath := "./receipt.pdf"
if len(os.Args) > 1 {
filePath = os.Args[1]
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
fmt.Printf("File not found: %s\n", filePath)
os.Exit(1)
}
_, err := processReceiptParseExtract(filePath)
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}// Deploy the "Receipt Parse + Extract" 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/receipt-parse-extract.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: receipt-parse-extract).
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, "receipt-parse-extract.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": "Receipt Parse + Extract Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": false
},
"signatureDetectionEnabled": false
},
"tables": {
"targetFormat": "markdown"
},
"figures": {
"enabled": false
},
"barcodes": {
"readingEnabled": false
},
"formulas": {
"enabled": false
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"required": [
"line_items",
"tax_amount",
"vendor_name",
"total_amount",
"vendor_email",
"vendor_phone",
"change_amount",
"customer_name",
"payment_method",
"receipt_number",
"vendor_address",
"subtotal_amount",
"transaction_date"
],
"properties": {
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"quantity",
"unit_price",
"description",
"total_price"
],
"properties": {
"quantity": {
"type": [
"number",
"null"
],
"description": "The number of units purchased for this line item. May 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 any quantity multiplication or discounts."
},
"description": {
"type": [
"string",
"null"
],
"description": "A description of the product or service purchased in this line item. May include item name, SKU, or other identifying details."
},
"total_price": {
"type": [
"number",
"null"
],
"description": "The total price for this line item, typically calculated as quantity multiplied by unit price, before taxes or discounts."
}
},
"additionalProperties": false
},
"description": "The individual products or services purchased in this transaction. Each item typically includes a description, quantity, unit price, and total price. Formats vary widely, 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 tax charged for this transaction. May be labeled as 'Tax', 'Sales Tax', 'VAT', or similar. If multiple taxes are present, this should be the combined total.",
"extend:type": "currency",
"additionalProperties": false
},
"vendor_name": {
"type": [
"string",
"null"
],
"description": "The name of the business, merchant, or entity that issued this receipt and received payment. This is the party providing goods or services."
},
"total_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 paid for this transaction, including all items, taxes, fees, and adjustments. This is the final payment amount and may be labeled as 'Total', 'Amount Paid', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"vendor_email": {
"type": [
"string",
"null"
],
"description": "The email address of the business or merchant issuing the receipt, if present."
},
"vendor_phone": {
"type": [
"string",
"null"
],
"description": "The phone number of the business or merchant issuing the receipt. May include country and area codes."
},
"change_amount": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The amount of change returned to the customer, if payment exceeded the total amount. May be labeled as 'Change', 'Cash Back', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"customer_name": {
"type": [
"string",
"null"
],
"description": "The name of the customer or purchaser, if specified on the receipt. May be labeled as 'Customer', 'Sold To', or similar."
},
"payment_method": {
"type": [
"string",
"null"
],
"description": "The method of payment used for this transaction, such as 'Credit Card', 'Cash', 'Debit', 'Mobile Payment', or specific card type. May include partial card numbers or other identifiers."
},
"receipt_number": {
"type": [
"string",
"null"
],
"description": "The unique identifier or reference number for this receipt. This may include numbers, letters, or special characters. Commonly labeled as 'Receipt #', 'Transaction ID', or similar, but the key is identifying the primary reference for this transaction."
},
"vendor_address": {
"type": [
"string",
"null"
],
"description": "The address of the business or merchant issuing the receipt. May include street, city, state, postal code, and country. Sometimes appears as a block of text or in multiple lines."
},
"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 item prices before taxes, discounts, or additional fees. May be labeled as 'Subtotal', 'Items Total', or similar.",
"extend:type": "currency",
"additionalProperties": false
},
"transaction_date": {
"type": [
"string",
"null"
],
"description": "The date when the transaction occurred and the receipt was issued. This is the official date for accounting and record-keeping purposes. May be labeled as 'Date', 'Transaction Date', or similar.",
"extend:type": "date"
}
},
"additionalProperties": false
},
"baseProcessor": "extraction_light",
"advancedOptions": {
"reviewAgent": {
"enabled": false
},
"advancedMultimodalEnabled": false
}
}
}
}
]
};
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 sys
import json
from pathlib import Path
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Error: Set EXTEND_API_KEY first.", file=sys.stderr)
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "receipt-parse-extract.json"
state = {}
if STATE_FILE.exists():
with open(STATE_FILE, "r") as f:
state = json.load(f)
def save_state():
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
WORKFLOW = {
"name": "Receipt Parse + Extract Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": False
},
"signatureDetectionEnabled": False
},
"tables": {
"targetFormat": "markdown"
},
"figures": {
"enabled": False
},
"barcodes": {
"readingEnabled": False
},
"formulas": {
"enabled": False
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"required": [
"line_items",
"tax_amount",
"vendor_name",
"total_amount",
"vendor_email",
"vendor_phone",
"change_amount",
"customer_name",
"payment_method",
"receipt_number",
"vendor_address",
"subtotal_amount",
"transaction_date"
],
"properties": {
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": [
"quantity",
"unit_price",
"description",
"total_price"
],
"properties": {
"quantity": {
"type": [
"number",
"null"
],
"description": "The number of units purchased for this line item. May 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 any quantity multiplication or discounts."
},
"description": {
"type": [
"string",
"null"
],
"description": "A description of the product or service purchased in this line item. May include item name, SKU, or other identifying details."
},
"total_price": {
"type": [
"number",
"null"
],
"description": "The total price for this line item, typically calculated as quantity multiplied by unit price, before taxes or discounts."
}
},
"additionalProperties": False
},
"description": "The individual products or services purchased in this transaction. Each item typically includes a description, quantity, unit price, and total price. Formats vary widely, 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 tax charged for this transaction. May be labeled as 'Tax', 'Sales Tax', 'VAT', or similar. If multiple taxes are present, this should be the combined total.",
"extend:type": "currency",
"additionalProperties": False
},
"vendor_name": {
"type": [
"string",
"null"
],
"description": "The name of the business, merchant, or entity that issued this receipt and received payment. This is the party providing goods or services."
},
"total_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 paid for this transaction, including all items, taxes, fees, and adjustments. This is the final payment amount and may be labeled as 'Total', 'Amount Paid', or similar.",
"extend:type": "currency",
"additionalProperties": False
},
"vendor_email": {
"type": [
"string",
"null"
],
"description": "The email address of the business or merchant issuing the receipt, if present."
},
"vendor_phone": {
"type": [
"string",
"null"
],
"description": "The phone number of the business or merchant issuing the receipt. May include country and area codes."
},
"change_amount": {
"type": "object",
"required": [
"amount",
"iso_4217_currency_code"
],
"properties": {
"amount": {
"type": [
"number",
"null"
]
},
"iso_4217_currency_code": {
"type": [
"string",
"null"
]
}
},
"description": "The amount of change returned to the customer, if payment exceeded the total amount. May be labeled as 'Change', 'Cash Back', or similar.",
"extend:type": "currency",
"additionalProperties": False
},
"customer_name": {
"type": [
"string",
"null"
],
"description": "The name of the customer or purchaser, if specified on the receipt. May be labeled as 'Customer', 'Sold To', or similar."
},
"payment_method": {
"type": [
"string",
"null"
],
"description": "The method of payment used for this transaction, such as 'Credit Card', 'Cash', 'Debit', 'Mobile Payment', or specific card type. May include partial card numbers or other identifiers."
},
"receipt_number": {
"type": [
"string",
"null"
],
"description": "The unique identifier or reference number for this receipt. This may include numbers, letters, or special characters. Commonly labeled as 'Receipt #', 'Transaction ID', or similar, but the key is identifying the primary reference for this transaction."
},
"vendor_address": {
"type": [
"string",
"null"
],
"description": "The address of the business or merchant issuing the receipt. May include street, city, state, postal code, and country. Sometimes appears as a block of text or in multiple lines."
},
"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 item prices before taxes, discounts, or additional fees. May be labeled as 'Subtotal', 'Items Total', or similar.",
"extend:type": "currency",
"additionalProperties": False
},
"transaction_date": {
"type": [
"string",
"null"
],
"description": "The date when the transaction occurred and the receipt was issued. This is the official date for accounting and record-keeping purposes. May be labeled as 'Date', 'Transaction Date', or similar.",
"extend:type": "date"
}
},
"additionalProperties": False
},
"baseProcessor": "extraction_light",
"advancedOptions": {
"reviewAgent": {
"enabled": False
},
"advancedMultimodalEnabled": False
}
}
}
}
]
}
def main():
client = Extend(token=API_KEY)
print(f'Deploying "{WORKFLOW["name"]}"\u2026')
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((w for w in items if w.name == WORKFLOW["name"]), None)
if existing and existing.id:
state["workflowId"] = existing.id
save_state()
print(f'✓ workflow "{WORKFLOW["name"]}" found in your account ({existing.id}) — updating steps')
client.workflows.update(id=existing.id, steps=WORKFLOW["steps"])
except Exception:
pass
if not state.get("workflowId"):
created = client.workflows.create(**WORKFLOW)
workflow_id = created.id if hasattr(created, 'id') else (created.workflow.id if hasattr(created, 'workflow') else None)
if not workflow_id:
raise Exception("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(f"Error: {str(e)}", file=sys.stderr)
sys.exit(1)// This script uses Extend's REST API directly because Extend has no official Java SDK yet.
// Call the API via java.net.http.HttpClient with no external dependencies.
import java.io.IOException;
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;
public class Provision {
private static final String API = "https://api.extend.ai";
private static final String VERSION = "2026-02-09";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static class State {
String workflowId;
static State load(Path file) throws IOException {
if (Files.exists(file)) {
String json = Files.readString(file);
State s = new State();
// Simple JSON parsing for workflowId
int idx = json.indexOf("\"workflowId\"");
if (idx >= 0) {
int colonIdx = json.indexOf(":", idx);
int quoteStart = json.indexOf("\"", colonIdx);
int quoteEnd = json.indexOf("\"", quoteStart + 1);
if (quoteStart >= 0 && quoteEnd > quoteStart) {
s.workflowId = json.substring(quoteStart + 1, quoteEnd);
}
}
return s;
}
return new State();
}
void save(Path file) throws IOException {
Files.createDirectories(file.getParent());
String json = workflowId != null ? String.format("{\"workflowId\":\"%s\"}", workflowId) : "{}";
Files.writeString(file, json);
}
}
private static String jsonString(String value) {
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
private static String getWorkflowJson() {
return """
{
"name": "Receipt Parse + Extract Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [{"step": "parse1"}]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {"agentic": {"enabled": false}, "signatureDetectionEnabled": false},
"tables": {"targetFormat": "markdown"},
"figures": {"enabled": false},
"barcodes": {"readingEnabled": false},
"formulas": {"enabled": false}
},
"chunkingStrategy": {"type": "document"}
}
},
"next": [{"step": "extraction2"}]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"required": ["line_items", "tax_amount", "vendor_name", "total_amount", "vendor_email", "vendor_phone", "change_amount", "customer_name", "payment_method", "receipt_number", "vendor_address", "subtotal_amount", "transaction_date"],
"properties": {
"line_items": {
"type": "array",
"items": {
"type": "object",
"required": ["quantity", "unit_price", "description", "total_price"],
"properties": {
"quantity": {"type": ["number", "null"], "description": "The number of units purchased for this line item. May 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 any quantity multiplication or discounts."},
"description": {"type": ["string", "null"], "description": "A description of the product or service purchased in this line item. May include item name, SKU, or other identifying details."},
"total_price": {"type": ["number", "null"], "description": "The total price for this line item, typically calculated as quantity multiplied by unit price, before taxes or discounts."}
},
"additionalProperties": false
},
"description": "The individual products or services purchased in this transaction. Each item typically includes a description, quantity, unit price, and total price. Formats vary widely, 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 tax charged for this transaction. May be labeled as 'Tax', 'Sales Tax', 'VAT', or similar. If multiple taxes are present, this should be the combined total.", "extend:type": "currency", "additionalProperties": false},
"vendor_name": {"type": ["string", "null"], "description": "The name of the business, merchant, or entity that issued this receipt and received payment. This is the party providing goods or services."},
"total_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 paid for this transaction, including all items, taxes, fees, and adjustments. This is the final payment amount and may be labeled as 'Total', 'Amount Paid', or similar.", "extend:type": "currency", "additionalProperties": false},
"vendor_email": {"type": ["string", "null"], "description": "The email address of the business or merchant issuing the receipt, if present."},
"vendor_phone": {"type": ["string", "null"], "description": "The phone number of the business or merchant issuing the receipt. May include country and area codes."},
"change_amount": {"type": "object", "required": ["amount", "iso_4217_currency_code"], "properties": {"amount": {"type": ["number", "null"]}, "iso_4217_currency_code": {"type": ["string", "null"]}}, "description": "The amount of change returned to the customer, if payment exceeded the total amount. May be labeled as 'Change', 'Cash Back', or similar.", "extend:type": "currency", "additionalProperties": false},
"customer_name": {"type": ["string", "null"], "description": "The name of the customer or purchaser, if specified on the receipt. May be labeled as 'Customer', 'Sold To', or similar."},
"payment_method": {"type": ["string", "null"], "description": "The method of payment used for this transaction, such as 'Credit Card', 'Cash', 'Debit', 'Mobile Payment', or specific card type. May include partial card numbers or other identifiers."},
"receipt_number": {"type": ["string", "null"], "description": "The unique identifier or reference number for this receipt. This may include numbers, letters, or special characters. Commonly labeled as 'Receipt #', 'Transaction ID', or similar, but the key is identifying the primary reference for this transaction."},
"vendor_address": {"type": ["string", "null"], "description": "The address of the business or merchant issuing the receipt. May include street, city, state, postal code, and country. Sometimes appears as a block of text or in multiple lines."},
"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 item prices before taxes, discounts, or additional fees. May be labeled as 'Subtotal', 'Items Total', or similar.", "extend:type": "currency", "additionalProperties": false},
"transaction_date": {"type": ["string", "null"], "description": "The date when the transaction occurred and the receipt was issued. This is the official date for accounting and record-keeping purposes. May be labeled as 'Date', 'Transaction Date', or similar.", "extend:type": "date"}
},
"additionalProperties": false
},
"baseProcessor": "extraction_light",
"advancedOptions": {"reviewAgent": {"enabled": false}, "advancedMultimodalEnabled": false}
}
}
}
]
}""";
}
private static String apiCall(HttpClient client, String method, String pathName, String body) throws IOException, InterruptedException {
HttpRequest.Builder rb = HttpRequest.newBuilder()
.uri(java.net.URI.create(API + pathName))
.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(body))
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
rb.header("Content-Type", "application/json");
}
HttpRequest req = rb.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() < 200 || res.statusCode() >= 300) {
String preview = res.body().length() > 300 ? res.body().substring(0, 300) : res.body();
throw new RuntimeException(method + " " + pathName + " failed (" + res.statusCode() + "): " + preview);
}
return res.body();
}
private static String extractJsonValue(String json, String key) {
String searchKey = "\"" + key + "\"";
int idx = json.indexOf(searchKey);
if (idx < 0) return null;
int colonIdx = json.indexOf(":", idx);
if (colonIdx < 0) return null;
int i = colonIdx + 1;
while (i < json.length() && Character.isWhitespace(json.charAt(i))) i++;
if (i >= json.length()) return null;
if (json.charAt(i) == '"') {
int start = i + 1;
int end = json.indexOf('"', start);
if (end < 0) return null;
return json.substring(start, end);
}
int end = i;
while (end < json.length() && json.charAt(end) != ',' && json.charAt(end) != '}' && json.charAt(end) != ']') {
end++;
}
return json.substring(i, end).trim();
}
public static void main(String[] args) {
if (API_KEY == null) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
try {
Path stateDir = Paths.get(System.getProperty("user.dir"), ".extend");
Path stateFile = stateDir.resolve("receipt-parse-extract.json");
State state = State.load(stateFile);
HttpClient client = HttpClient.newHttpClient();
String workflowName = "Receipt Parse + Extract Processing Pipeline";
System.out.println("Deploying \"" + workflowName + "\"…");
if (state.workflowId != null) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
String stepsJson = getWorkflowJson();
int stepsIdx = stepsJson.indexOf("\"steps\"");
int stepsStart = stepsJson.indexOf("[", stepsIdx);
int stepsEnd = stepsJson.lastIndexOf("]");
String steps = stepsJson.substring(stepsStart, stepsEnd + 1);
String updateBody = "{\"steps\":" + steps + "}";
apiCall(client, "POST", "/workflows/" + state.workflowId, updateBody);
} else {
boolean found = false;
try {
String encoded = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
String listRes = apiCall(client, "GET", "/workflows?name=" + encoded, null);
String idStr = extractJsonValue(listRes, "id");
if (idStr != null) {
state.workflowId = idStr;
state.save(stateFile);
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + idStr + ") — updating steps");
String stepsJson = getWorkflowJson();
int stepsIdx = stepsJson.indexOf("\"steps\"");
int stepsStart = stepsJson.indexOf("[", stepsIdx);
int stepsEnd = stepsJson.lastIndexOf("]");
String steps = stepsJson.substring(stepsStart, stepsEnd + 1);
String updateBody = "{\"steps\":" + steps + "}";
apiCall(client, "POST", "/workflows/" + state.workflowId, updateBody);
found = true;
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (!found) {
String createRes = apiCall(client, "POST", "/workflows", getWorkflowJson());
String wfId = extractJsonValue(createRes, "id");
if (wfId == null) {
throw new RuntimeException("Could not read created workflow id from response");
}
state.workflowId = wfId;
state.save(stateFile);
System.out.println("+ created workflow (" + wfId + ")");
}
}
// Deploy the current draft as a new version so the workflow is runnable
try {
apiCall(client, "POST", "/workflows/" + state.workflowId + "/versions", "{}");
} catch (Exception e) {
// best-effort; some accounts/plans may not require this explicit step
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + state.workflowId + "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
} catch (Exception e) {
System.err.println(e.getMessage() != null ? e.getMessage() : e.toString());
System.exit(1);
}
}
}// This script uses the Extend REST API directly because Extend has no official Go SDK.
// Deploy the "Receipt Parse + Extract" 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: receipt-parse-extract).
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
var (
apiKey = os.Getenv("EXTEND_API_KEY")
stateDir = filepath.Join(".", ".extend")
stateFile = filepath.Join(stateDir, "receipt-parse-extract.json")
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
var state State
func init() {
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
data, err := os.ReadFile(stateFile)
if err == nil {
_ = json.Unmarshal(data, &state)
}
}
func saveState() error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, _ := json.MarshalIndent(state, "", " ")
return os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
var bodyReader io.Reader
var contentType string
if body != nil {
data, _ := json.Marshal(body)
bodyReader = bytes.NewReader(data)
contentType = "application/json"
}
req, _ := http.NewRequest(method, API+pathName, bodyReader)
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("x-extend-api-version", VERSION)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
_ = json.Unmarshal(respBody, &result)
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 result, nil
}
var workflow = map[string]interface{}{
"name": "Receipt Parse + Extract 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": false,
},
"signatureDetectionEnabled": false,
},
"tables": map[string]interface{}{
"targetFormat": "markdown",
},
"figures": map[string]interface{}{
"enabled": false,
},
"barcodes": map[string]interface{}{
"readingEnabled": false,
},
"formulas": map[string]interface{}{
"enabled": false,
},
},
"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",
"required": []string{
"line_items", "tax_amount", "vendor_name", "total_amount",
"vendor_email", "vendor_phone", "change_amount", "customer_name",
"payment_method", "receipt_number", "vendor_address", "subtotal_amount",
"transaction_date",
},
"properties": map[string]interface{}{
"line_items": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"required": []string{"quantity", "unit_price", "description", "total_price"},
"properties": map[string]interface{}{
"quantity": map[string]interface{}{
"type": []string{"number", "null"},
"description": "The number of units purchased for this line item. May 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 any quantity multiplication or discounts.",
},
"description": map[string]interface{}{
"type": []string{"string", "null"},
"description": "A description of the product or service purchased in this line item. May include item name, SKU, or other identifying details.",
},
"total_price": map[string]interface{}{
"type": []string{"number", "null"},
"description": "The total price for this line item, typically calculated as quantity multiplied by unit price, before taxes or discounts.",
},
},
"additionalProperties": false,
},
"description": "The individual products or services purchased in this transaction. Each item typically includes a description, quantity, unit price, and total price. Formats vary widely, from tables to lists or other structures.",
},
"tax_amount": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"description": "The total tax charged for this transaction. May be labeled as 'Tax', 'Sales Tax', 'VAT', or similar. If multiple taxes are present, this should be the combined total.",
"extend:type": "currency",
"additionalProperties": false,
},
"vendor_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the business, merchant, or entity that issued this receipt and received payment. This is the party providing goods or services.",
},
"total_amount": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"description": "The total amount paid for this transaction, including all items, taxes, fees, and adjustments. This is the final payment amount and may be labeled as 'Total', 'Amount Paid', or similar.",
"extend:type": "currency",
"additionalProperties": false,
},
"vendor_email": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The email address of the business or merchant issuing the receipt, if present.",
},
"vendor_phone": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The phone number of the business or merchant issuing the receipt. May include country and area codes.",
},
"change_amount": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"description": "The amount of change returned to the customer, if payment exceeded the total amount. May be labeled as 'Change', 'Cash Back', or similar.",
"extend:type": "currency",
"additionalProperties": false,
},
"customer_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The name of the customer or purchaser, if specified on the receipt. May be labeled as 'Customer', 'Sold To', or similar.",
},
"payment_method": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The method of payment used for this transaction, such as 'Credit Card', 'Cash', 'Debit', 'Mobile Payment', or specific card type. May include partial card numbers or other identifiers.",
},
"receipt_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The unique identifier or reference number for this receipt. This may include numbers, letters, or special characters. Commonly labeled as 'Receipt #', 'Transaction ID', or similar, but the key is identifying the primary reference for this transaction.",
},
"vendor_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The address of the business or merchant issuing the receipt. May include street, city, state, postal code, and country. Sometimes appears as a block of text or in multiple lines.",
},
"subtotal_amount": map[string]interface{}{
"type": "object",
"required": []string{"amount", "iso_4217_currency_code"},
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"iso_4217_currency_code": map[string]interface{}{"type": []string{"string", "null"}},
},
"description": "The sum of all item prices before taxes, discounts, or additional fees. May be labeled as 'Subtotal', 'Items Total', or similar.",
"extend:type": "currency",
"additionalProperties": false,
},
"transaction_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "The date when the transaction occurred and the receipt was issued. This is the official date for accounting and record-keeping purposes. May be labeled as 'Date', 'Transaction Date', or similar.",
"extend:type": "date",
},
},
"additionalProperties": false,
},
"baseProcessor": "extraction_light",
"advancedOptions": map[string]interface{}{
"reviewAgent": map[string]interface{}{
"enabled": false,
},
"advancedMultimodalEnabled": false,
},
},
},
},
},
}
func main() {
fmt.Printf("Deploying \"%s\"…\n", workflow["name"])
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
_, _ = apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{"steps": workflow["steps"]})
} else {
found := false
list, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", url.QueryEscape(workflow["name"].(string))), nil)
if err == nil {
var items []map[string]interface{}
if data, ok := list["data"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
} else if data, ok := list["items"].([]interface{}); ok {
for _, item := range data {
items = append(items, item.(map[string]interface{}))
}
}
for _, item := range items {
if 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)
_, _ = apiCall("POST", fmt.Sprintf("/workflows/%s", id), map[string]interface{}{"steps": workflow["steps"]})
found = true
break
}
}
}
}
if !found {
created, err := apiCall("POST", "/workflows", workflow)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %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, "Error: Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
_ = saveState()
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
_, _ = apiCall("POST", 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.")
}
import (
"net/http"
"net/url"
)This template processes point-of-sale tax invoices and cash receipts from retail businesses. It captures merchant information, line-item details with quantities and prices, payment amounts, and change due.