Parses receipts into markdown .
A retail receipt is a point-of-sale transaction document issued by a merchant that records store information, itemized products with prices, tax calculations, payment method, and authorization codes for a customer purchase. This template takes in Retail Receipt and outputs JSON (.json) with structured transaction fields including store metadata, itemized line items, financial totals, and payment confirmation details per the extraction schema by using Extend's Parse primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
chunks[{"id":"chunk_section_4fMbrk","type":"section","blocks":[{"id":"block_1_ocr_NRsDLA","type":"text","object":"block","content":"G","details":{},"polygon":[{"x":57…changedparseOutputMetadata.finalMimeType"image/jpeg"changedparseOutputMetadata.originalMimeType"image/jpeg"changedparseOutputMetadata.pagesnullchangedYou can learn more about Parse configuration in Extend's Parse documentation.
{
"name": "Receipt Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
},
"signatureDetectionEnabled": true
},
"tables": {
"agentic": {
"enabled": true
},
"tableHeaderContinuationEnabled": true
},
"figures": {
"enabled": true
}
},
"chunkingStrategy": {
"type": "page",
"options": {}
}
}
}
}
]
}# Receipt Processing — Extend AI Skill
## What this pipeline does
This pipeline parses retail receipts (point-of-sale printouts) into structured markdown, capturing store information, itemized products with prices, subtotals, taxes, and payment authorization details. It handles variable-length item lists, table-based layouts, and embedded card authentication codes common in physical receipts. Output is markdown with precise bounding boxes for agentic workflows.
## When to use this
- **Expense management systems**: Employees submit receipt images; you parse to extract vendor, date, total, and line items for reimbursement.
- **Retail auditing**: Chain stores need to digitize paper receipts to verify inventory, pricing, and payment records.
- **Financial reconciliation**: Accounting teams match receipts to bank transactions by extracting transaction ID, timestamp, and payment method.
- **POS data extraction**: Legacy point-of-sale systems that print rather than emit JSON; convert to structured data.
- **Multi-store loyalty programs**: Parse Clubcard/loyalty statements printed on receipts to track member points and promotions.
## Processor pipeline
### Step 1: Parse (agentic_ocr mode)
**Processor**: `parseRuns.createAndPoll()`
**Purpose**: Convert receipt image to markdown with tables and text blocks preserved; enable agentic OCR to handle low-contrast thermal paper and skewed camera angles.
**Key config**:
- `blockOptions.text.agentic.enabled: true` — use AI-enhanced OCR for blurry/faded thermal receipts
- `blockOptions.tables.agentic.enabled: true` — reconstruct corrupted item tables from context
- `blockOptions.tables.tableHeaderContinuationEnabled: true` — continue table header across pages if receipt is multi-page
- `blockOptions.figures.enabled: true` — preserve QR codes, logos, barcodes as figures for manual review
- `chunkingStrategy: "page"` — one chunk per page; receipts are short so this matches 1:1
**Why**: Receipts are often printed on thermal paper with faded ink and captured at odd angles. Agentic mode leverages LLM-aware OCR to infer missing digits (e.g., `£___.89` → `£2.89` from context). Table reconstruction recovers alignment lost in poor scans.
## TypeScript implementation
## CLI equivalent
```bash
# Step 1: Parse receipt to markdown (agentic OCR for thermal paper)
extend parse receipt.jpg \
--mode agentic_ocr \
--output-type markdown \
--agentic-text \
--agentic-tables \
--signatures \
--figures
# Step 2: Extract structured fields (requires schema.json)
extend extract receipt.jpg --schema receipt-schema.json
```
**receipt-schema.json** (use with `extend extract`):
```json
{
"type": "object",
"properties": {
"store_name": {
"type": ["string", "null"],
"description": "Name of the retail store or merchant"
},
"transaction_date": {
"type": ["string", "null"],
"description": "Transaction date in DD/MM/YY or ISO 8601 format"
},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": {
"type": ["string", "null"],
"description": "Product name or description"
},
"line_total": {
"type": ["string", "null"],
"description": "Total for this line item, e.g. '£2.29' or '2.29'"
}
}
},
"description": "Array of purchased items with prices"
},
"total_amount": {
"type": ["string", "null"],
"description": "Total amount due including tax, e.g. '£5.08'"
},
"payment_method": {
"type": ["string", "null"],
"description": "Payment method: CARD, CASH, VOUCHER, CLUBCARD"
},
"card_last_four": {
"type": ["string", "null"],
"description": "Last 4 digits of card (e.g. '0938')"
},
"authorization_code": {
"type": ["string", "null"],
"description": "Payment authorization code from terminal (e.g. '036017')"
}
}
}
```
## Schema
The extraction schema captures retail receipt structure end-to-end:
```typescript
z.object({
// Store & transaction metadata
store_name: z.string().nullable()
.describe("Store name—critical for matching receipts to vendor in accounting system"),
store_phone: z.string().nullable()
.describe("Store phone for dispute resolution; helps verify location"),
transaction_date: z.string().nullable()
.describe("Date in DD/MM/YY or ISO format; essential for matching bank feed timestamps"),
transaction_time: z.string().nullable()
.describe("Time HH:MM; helps reconcile multi-transaction days"),
// Line items—HIGHEST accuracy lever
line_items: z.array(z.object({
description: z.string().nullable()
.describe("Product name. CRITICAL: infer from context if garbled. '2 @ £0.95' → qty=2, unit=0.95, total=1.90"),
quantity: z.string().nullable()
.describe("Qty notation: '2 @', '1x', '2 x £1.50', or null if single item. Preserve original format for POS reconciliation."),
unit_price: extendCurrency()
.describe("Unit price if shown separately; null if only total shown. Used for inventory audits."),
line_total: extendCurrency()
.describe("Line total (qty × unit or shown total). This is the row sum; must not include tax."),
}))
.describe("Item array. Why detailed: expense systems require line-level audit trail. If item qty unclear, assume 1."),
// Totals
subtotal: extendCurrency()
.describe("Subtotal BEFORE tax; required for VAT reconciliation and tax liability reports"),
tax_amount: extendCurrency()
.describe("Tax/VAT amount; verify against subtotal × tax_rate for compliance audits"),
total_amount: extendCurrency()
.describe("Grand total (subtotal + tax). This is what was charged to payment method."),
// Payment & authorization
payment_method: z.string().nullable()
.describe("CARD, CASH, VOUCHER, CHEQUE, CLUBCARD, or other. Directs routing in accounting."),
card_type: z.string().nullable()
.describe("Card brand: VISA, MASTERCARD, AMEX, JCB. Helps match card statement lines."),
card_last_four: z.string().nullable()
.describe("Last 4 digits masked (e.g. '1234'). Links to bank reconciliation and fraud detection."),
authorization_code: z.string().nullable()
.describe("Auth code from card terminal (e.g. '036017'). Proof of authorization; required for disputes."),
merchant_id: z.string().nullable()
.describe("Merchant ID from payment processor. Reconciles receipt to processor statement."),
aid: z.string().nullable()
.describe("EMV AID from chip reader (e.g. A0000000041010 for Mastercard).import { ExtendClient, extendCurrency } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
/**
* Process a receipt image: parse to markdown, then extract structured fields.
* Handles thermal paper, low contrast, and variable-length item lists.
*/
async function processReceipt(filePath: string) {
// Convert local file to data URL for SDK
const fileBuffer = fs.readFileSync(filePath);
const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;
console.log(`[1/2] Parsing receipt from ${filePath}...`);
// Step 1: Parse receipt to markdown + bounding boxes
// Agentic OCR mode handles thermal paper, faded text, skewed images
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
mode: "agentic_ocr",
outputType: "markdown",
blockOptions: {
text: {
agentic: { enabled: true },
signatureDetectionEnabled: true, // Detect cardholder signatures
},
tables: {
agentic: { enabled: true },
tableHeaderContinuationEnabled: true, // Handle multi-page receipts
},
figures: { enabled: true }, // Preserve barcodes, QR codes, logos
},
chunkingStrategy: {
type: "page",
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
// Combine all chunks into single markdown document
const receiptMarkdown = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n---\n\n");
console.log(`[1/2] ✓ Parse complete. Receipt length: ${receiptMarkdown.length} chars`);
console.log(`Sample:\n${receiptMarkdown.substring(0, 300)}...\n`);
// Step 2: Extract structured receipt fields using Zod schema
// This pattern is production-ready: fully typed, reusable schema
console.log(`[2/2] Extracting structured fields...`);
const receiptSchema = z.object({
store_name: z.string().nullable().describe("Name of the retail store or merchant"),
store_phone: z
.string()
.nullable()
.describe("Store phone number if printed on receipt"),
transaction_date: z
.string()
.nullable()
.describe("Transaction date in format DD/MM/YY or ISO 8601"),
transaction_time: z.string().nullable().describe("Transaction time in HH:MM format"),
line_items: z
.array(
z.object({
description: z
.string()
.nullable()
.describe("Product name or description from receipt line"),
quantity: z
.string()
.nullable()
.describe("Quantity if shown (e.g. '2 @' or '1x'), null if single item"),
unit_price: extendCurrency().describe("Price per unit if shown separately"),
line_total: extendCurrency().describe("Total for this line item"),
})
)
.describe(
"Array of purchased items with prices. Critical: infer missing fields from context (e.g., qty=1 if not stated)"
),
subtotal: extendCurrency().describe("Subtotal before tax"),
tax_amount: extendCurrency().describe("Tax/VAT amount"),
total_amount: extendCurrency().describe("Total amount due (subtotal + tax)"),
payment_method: z
.string()
.nullable()
.describe("Payment method: CARD, CASH, VOUCHER, CLUBCARD, etc."),
card_type: z.string().nullable().describe("Card type if card payment: VISA, MASTERCARD, AMEX"),
card_last_four: z
.string()
.nullable()
.describe("Last 4 digits of card number (masked as ****1234)"),
authorization_code: z
.string()
.nullable()
.describe("Payment authorization/approval code from card terminal"),
merchant_id: z
.string()
.nullable()
.describe("Merchant ID from payment processor"),
aid: z
.string()
.nullable()
.describe(
"Application Identifier (AID) from EMV chip reader, e.g. A0000000041010 for Mastercard"
),
change_due: extendCurrency().describe("Change amount if cash payment"),
loyalty_program: z
.string()
.nullable()
.describe("Loyalty program name if present (e.g. TESCO CLUBCARD)"),
loyalty_number: z
.string()
.nullable()
.describe("Loyalty card number (may be partially masked)"),
loyalty_points_earned: z
.number()
.nullable()
.describe("Loyalty points earned this transaction"),
loyalty_points_total: z
.number()
.nullable()
.describe("Total loyalty points on account as of receipt date"),
});
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: receiptSchema,
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extract failed with status: ${extractRun.status}`);
}
const receipt = extractRun.output.value;
console.log(`[2/2] ✓ Extraction complete.\n`);
// Pretty print results
console.log("=== RECEIPT SUMMARY ===");
console.log(`Store: ${receipt.store_name || "N/A"}`);
console.log(`Date/Time: ${receipt.transaction_date || "N/A"} ${receipt.transaction_time || ""}`);
console.log(`\nPayment Method: ${receipt.payment_method || "N/A"}`);
if (receipt.card_type) {
console.log(` Card: ${receipt.card_type} (***${receipt.card_last_four || "****"})`);
console.log(` Auth Code: ${receipt.authorization_code || "N/A"}`);
}
if (receipt.loyalty_program) {
console.log(`\nLoyalty: ${receipt.loyalty_program}`);
console.log(` Points This Visit: ${receipt.loyalty_points_earned || 0}`);
console.log(` Total Points: ${receipt.loyalty_points_total || 0}`);
}
console.log(`\n=== ITEMS (${receipt.line_items.length}) ===`);
for (const item of receipt.line_items) {
const qty = item.quantity ? `${item.quantity} × ` : "";
const unitPrice =
item.unit_price.amount !== null
? `£${item.unit_price.amount.toFixed(2)}`
: "";
const lineTotal =
item.line_total.amount !== null
? `£${item.line_total.amount.toFixed(2)}`
: "—";
console.log(` ${item.description || "Unknown"} ${qty}${unitPrice} = ${lineTotal}`);
}
console.log(`\n=== TOTALS ===`);
console.log(
`Subtotal: £${receipt.subtotal.amount?.toFixed(2) || "0.00"}`
);
console.log(
`Tax: £${receipt.tax_amount.amount?.toFixed(2) || "0.00"}`
);
console.log(
`TOTAL: £${receipt.total_amount.amount?.toFixed(2) || "0.00"}`
);
if (receipt.change_due && receipt.change_due.amount !== null) {
console.log(
`Change: £${receipt.change_due.amount.toFixed(2)}`
);
}
// Return structured data for downstream processing
return {
status: "success",
markdown: receiptMarkdown,
receipt,
};
}
// Main entry point
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: npx ts-node solution.ts <receipt-image-path>");
process.exit(1);
}
processReceipt(args[0]).catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});import os
import sys
import base64
from extend_ai import Extend
from zod import z
client = Extend(token=os.environ["EXTEND_API_KEY"])
def process_receipt(file_path: str):
"""
Process a receipt image: parse to markdown, then extract structured fields.
Handles thermal paper, low contrast, and variable-length item lists.
"""
# Convert local file to data URL for SDK
with open(file_path, "rb") as f:
file_buffer = f.read()
data_url = f"data:application/octet-stream;base64,{base64.b64encode(file_buffer).decode()}"
print(f"[1/2] Parsing receipt from {file_path}...")
# Step 1: Parse receipt to markdown + bounding boxes
# Agentic OCR mode handles thermal paper, faded text, skewed images
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"mode": "agentic_ocr",
"output_type": "markdown",
"block_options": {
"text": {
"agentic": {"enabled": True},
"signature_detection_enabled": True,
},
"tables": {
"agentic": {"enabled": True},
"table_header_continuation_enabled": True,
},
"figures": {"enabled": True},
},
"chunking_strategy": {
"type": "page",
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
# Combine all chunks into single markdown document
receipt_markdown = "\n\n---\n\n".join(
chunk.content for chunk in parse_run.output.chunks
)
print(f"[1/2] ✓ Parse complete. Receipt length: {len(receipt_markdown)} chars")
print(f"Sample:\n{receipt_markdown[:300]}...\n")
# Step 2: Extract structured receipt fields using schema
print("[2/2] Extracting structured fields...")
receipt_schema = {
"type": "object",
"properties": {
"store_name": {
"type": ["string", "null"],
"description": "Name of the retail store",
},
"store_location": {
"type": ["string", "null"],
"description": "Store address including street, city, state, and zip code",
},
"store_phone": {
"type": ["string", "null"],
"description": "Store contact phone number",
},
"transaction_date": {
"type": ["string", "null"],
"description": "Date of purchase in MM/DD/YY format",
},
"transaction_time": {
"type": ["string", "null"],
"description": "Time of purchase in HH:MM:SS format",
},
"items": {
"type": "array",
"description": "List of purchased items",
"items": {
"type": "object",
"properties": {
"description": {
"type": ["string", "null"],
"description": "Item name or description",
},
"quantity": {
"type": ["number", "null"],
"description": "Quantity of items purchased",
},
"unit_price": {
"type": ["number", "null"],
"description": "Price per unit",
},
},
},
},
"subtotal": {
"type": ["number", "null"],
"description": "Subtotal before tax",
},
"tax": {
"type": ["number", "null"],
"description": "Total tax amount",
},
"total": {
"type": ["number", "null"],
"description": "Final total amount due",
},
"payment_method": {
"type": ["string", "null"],
"description": "Payment method used (e.g., Chase Visa)",
},
"approval_code": {
"type": ["string", "null"],
"description": "Transaction approval code",
},
"transaction_reference": {
"type": ["string", "null"],
"description": "Transaction reference or confirmation number",
},
},
}
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": receipt_schema,
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extract failed with status: {extract_run.status}")
receipt = extract_run.output.value
print("[2/2] ✓ Extraction complete.\n")
# Pretty print results
print("=== RECEIPT SUMMARY ===")
print(f"Store: {receipt.get('store_name') or 'N/A'}")
print(
f"Location: {receipt.get('store_location') or 'N/A'}"
)
print(
f"Date/Time: {receipt.get('transaction_date') or 'N/A'} {receipt.get('transaction_time') or ''}"
)
print(f"\nPayment Method: {receipt.get('payment_method') or 'N/A'}")
if receipt.get("approval_code"):
print(f" Approval Code: {receipt.get('approval_code')}")
if receipt.get("transaction_reference"):
print(f" Reference: {receipt.get('transaction_reference')}")
items = receipt.get("items", [])
print(f"\n=== ITEMS ({len(items)}) ===")
for item in items:
description = item.get("description") or "Unknown"
quantity = item.get("quantity")
unit_price = item.get("unit_price")
qty_str = f"{quantity} × " if quantity else ""
price_str = f"${unit_price:.2f}" if unit_price is not None else ""
print(f" {description} {qty_str}{price_str}")
print("\n=== TOTALS ===")
subtotal = receipt.get("subtotal")
tax = receipt.get("tax")
total = receipt.get("total")
print(f"Subtotal: ${subtotal:.2f if subtotal is not None else '0.00'}")
print(f"Tax: ${tax:.2f if tax is not None else '0.00'}")
print(f"TOTAL: ${total:.2f if total is not None else '0.00'}")
# Return structured data for downstream processing
return {
"status": "success",
"markdown": receipt_markdown,
"receipt": receipt,
}
# Main entry point
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python solution.py <receipt-image-path>")
sys.exit(1)
try:
result = process_receipt(sys.argv[1])
print("\nProcessing complete!")
except Exception as err:
print(f"Error: {err}")
sys.exit(1)// This code uses Extend's REST API directly because Extend has no official Java SDK yet.
// It calls https://api.extend.ai endpoints with Bearer token authentication.
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.*;
public class ReceiptProcessor {
private static final String API_BASE = "https://api.extend.ai";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final HttpClient httpClient = HttpClient.newHttpClient();
static class CurrencyValue {
public Double amount;
public String currency;
public CurrencyValue(Double amount, String currency) {
this.amount = amount;
this.currency = currency;
}
}
static class LineItem {
public String description;
public String quantity;
public CurrencyValue unit_price;
public CurrencyValue line_total;
}
static class Receipt {
public String store_name;
public String store_phone;
public String transaction_date;
public String transaction_time;
public List<LineItem> line_items;
public CurrencyValue subtotal;
public CurrencyValue tax_amount;
public CurrencyValue total_amount;
public String payment_method;
public String card_type;
public String card_last_four;
public String authorization_code;
public String merchant_id;
public String aid;
public CurrencyValue change_due;
public String loyalty_program;
public String loyalty_number;
public Integer loyalty_points_earned;
public Integer loyalty_points_total;
}
static class ParseRunResponse {
public String status;
public ParseOutput output;
}
static class ParseOutput {
public List<Chunk> chunks;
}
static class Chunk {
public String content;
}
static class ExtractRunResponse {
public String status;
public ExtractOutput output;
}
static class ExtractOutput {
public Receipt value;
}
private static String fileToDataUrl(String filePath) throws IOException {
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
String base64 = Base64.getEncoder().encodeToString(fileBytes);
return "data:application/octet-stream;base64," + base64;
}
private static String makeRequest(String method, String endpoint, String jsonBody)
throws IOException, InterruptedException {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + endpoint))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json");
if ("POST".equals(method)) {
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else if ("GET".equals(method)) {
requestBuilder.GET();
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new IOException("API error: " + response.statusCode() + " " + response.body());
}
return response.body();
}
private static String buildParseConfig(String dataUrl) {
return "{"
+ "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
+ "\"config\":{"
+ "\"mode\":\"agentic_ocr\","
+ "\"outputType\":\"markdown\","
+ "\"blockOptions\":{"
+ "\"text\":{\"agentic\":{\"enabled\":true},\"signatureDetectionEnabled\":true},"
+ "\"tables\":{\"agentic\":{\"enabled\":true},\"tableHeaderContinuationEnabled\":true},"
+ "\"figures\":{\"enabled\":true}"
+ "},"
+ "\"chunkingStrategy\":{\"type\":\"page\"}"
+ "}"
+ "}";
}
private static String buildExtractConfig(String dataUrl) {
return "{"
+ "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
+ "\"config\":{"
+ "\"schema\":{"
+ "\"type\":\"object\","
+ "\"properties\":{"
+ "\"store_name\":{\"type\":[\"string\",\"null\"],\"description\":\"Name of the retail store or merchant\"},"
+ "\"store_phone\":{\"type\":[\"string\",\"null\"],\"description\":\"Store phone number if printed on receipt\"},"
+ "\"transaction_date\":{\"type\":[\"string\",\"null\"],\"description\":\"Transaction date in format DD/MM/YY or ISO 8601\"},"
+ "\"transaction_time\":{\"type\":[\"string\",\"null\"],\"description\":\"Transaction time in HH:MM format\"},"
+ "\"line_items\":{\"type\":\"array\",\"description\":\"Array of purchased items with prices\",\"items\":{\"type\":\"object\",\"properties\":{"
+ "\"description\":{\"type\":[\"string\",\"null\"],\"description\":\"Product name or description from receipt line\"},"
+ "\"quantity\":{\"type\":[\"string\",\"null\"],\"description\":\"Quantity if shown\"},"
+ "\"unit_price\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":[\"number\",\"null\"]},\"currency\":{\"type\":\"string\"}},\"description\":\"Price per unit if shown separately\"},"
+ "\"line_total\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":[\"number\",\"null\"]},\"currency\":{\"type\":\"string\"}},\"description\":\"Total for this line item\"}"
+ "}}},"
+ "\"subtotal\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":[\"number\",\"null\"]},\"currency\":{\"type\":\"string\"}},\"description\":\"Subtotal before tax\"},"
+ "\"tax_amount\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":[\"number\",\"null\"]},\"currency\":{\"type\":\"string\"}},\"description\":\"Tax/VAT amount\"},"
+ "\"total_amount\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":[\"number\",\"null\"]},\"currency\":{\"type\":\"string\"}},\"description\":\"Total amount due\"},"
+ "\"payment_method\":{\"type\":[\"string\",\"null\"],\"description\":\"Payment method: CARD, CASH, VOUCHER, CLUBCARD, etc.\"},"
+ "\"card_type\":{\"type\":[\"string\",\"null\"],\"description\":\"Card type if card payment: VISA, MASTERCARD, AMEX\"},"
+ "\"card_last_four\":{\"type\":[\"string\",\"null\"],\"description\":\"Last 4 digits of card number\"},"
+ "\"authorization_code\":{\"type\":[\"string\",\"null\"],\"description\":\"Payment authorization/approval code\"},"
+ "\"merchant_id\":{\"type\":[\"string\",\"null\"],\"description\":\"Merchant ID from payment processor\"},"
+ "\"aid\":{\"type\":[\"string\",\"null\"],\"description\":\"Application Identifier (AID) from EMV chip reader\"},"
+ "\"change_due\":{\"type\":[\"object\",\"null\"],\"properties\":{\"amount\":{\"type\":[\"number\",\"null\"]},\"currency\":{\"type\":\"string\"}},\"description\":\"Change amount if cash payment\"},"
+ "\"loyalty_program\":{\"type\":[\"string\",\"null\"],\"description\":\"Loyalty program name if present\"},"
+ "\"loyalty_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Loyalty card number\"},"
+ "\"loyalty_points_earned\":{\"type\":[\"number\",\"null\"],\"description\":\"Loyalty points earned this transaction\"},"
+ "\"loyalty_points_total\":{\"type\":[\"number\",\"null\"],\"description\":\"Total loyalty points on account\"}"
+ "}"
+ "}"
+ "}"
+ "}";
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
private static ParseRunResponse pollParseRun(String runId) throws IOException, InterruptedException {
long startTime = System.currentTimeMillis();
long timeout = 300000; // 5 minutes
while (System.currentTimeMillis() - startTime < timeout) {
String response = makeRequest("GET", "/v1/parseRuns/" + runId, null);
ParseRunResponse parseResp = parseJson(response, ParseRunResponse.class);
if ("PROCESSED".equals(parseResp.status)) {
return parseResp;
} else if ("FAILED".equals(parseResp.status) || "ERROR".equals(parseResp.status)) {
throw new IOException("Parse run failed with status: " + parseResp.status);
}
Thread.sleep(2000);
}
throw new IOException("Parse run polling timeout");
}
private static ExtractRunResponse pollExtractRun(String runId) throws IOException, InterruptedException {
long startTime = System.currentTimeMillis();
long timeout = 300000; // 5 minutes
while (System.currentTimeMillis() - startTime < timeout) {
String response = makeRequest("GET", "/v1/extractRuns/" + runId, null);
ExtractRunResponse extractResp = parseJson(response, ExtractRunResponse.class);
if ("PROCESSED".equals(extractResp.status)) {
return extractResp;
} else if ("FAILED".equals(extractResp.status) || "ERROR".equals(extractResp.status)) {
throw new IOException("Extract run failed with status: " + extractResp.status);
}
Thread.sleep(2000);
}
throw new IOException("Extract run polling timeout");
}
private static <T> T parseJson(String json, Class<T> clazz) {
// Simple JSON parsing using reflection and string manipulation
// For production, consider using a JSON library
if (clazz == ParseRunResponse.class) {
ParseRunResponse resp = new ParseRunResponse();
resp.status = extractJsonString(json, "status");
resp.output = new ParseOutput();
resp.output.chunks = new ArrayList<>();
String chunksStr = extractJsonArray(json, "chunks");
for (String chunkStr : splitJsonArray(chunksStr)) {
Chunk chunk = new Chunk();
chunk.content = extractJsonString(chunkStr, "content");
resp.output.chunks.add(chunk);
}
return (T) resp;
} else if (clazz == ExtractRunResponse.class) {
ExtractRunResponse resp = new ExtractRunResponse();
resp.status = extractJsonString(json, "status");
resp.output = new ExtractOutput();
String valueStr = extractJsonObject(json, "value");
resp.output.value = parseReceipt(valueStr);
return (T) resp;
}
return null;
}
private static Receipt parseReceipt(String json) {
Receipt receipt = new Receipt();
receipt.store_name = extractJsonString(json, "store_name");
receipt.store_phone = extractJsonString(json, "store_phone");
receipt.transaction_date = extractJsonString(json, "transaction_date");
receipt.transaction_time = extractJsonString(json, "transaction_time");
receipt.payment_method = extractJsonString(json, "payment_method");
receipt.card_type = extractJsonString(json, "card_type");
receipt.card_last_four = extractJsonString(json, "card_last_four");
receipt.authorization_code = extractJsonString(json, "authorization_code");
receipt.merchant_id = extractJsonString(json, "merchant_id");
receipt.aid = extractJsonString(json, "aid");
receipt.loyalty_program = extractJsonString(json, "loyalty_program");
receipt.loyalty_number = extractJsonString(json, "loyalty_number");
receipt.loyalty_points_earned = extractJsonNumber(json, "loyalty_points_earned");
receipt.loyalty_points_total = extractJsonNumber(json, "loyalty_points_total");
receipt.subtotal = extractCurrency(json, "subtotal");
receipt.tax_amount = extractCurrency(json, "tax_amount");
receipt.total_amount = extractCurrency(json, "total_amount");
receipt.change_due = extractCurrency(json, "change_due");
receipt.line_items = new ArrayList<>();
String itemsStr = extractJsonArray(json, "line_items");
for (String itemStr : splitJsonArray(itemsStr)) {
LineItem item = new LineItem();
item.description = extractJsonString(itemStr, "description");
item.quantity = extractJsonString(itemStr, "quantity");
item.unit_price = extractCurrency(itemStr, "unit_price");
item.line_total = extractCurrency(itemStr, "line_total");
receipt.line_items.add(item);
}
return receipt;
}
private static CurrencyValue extractCurrency(String json, String key) {
String objStr = extractJsonObject(json, key);
if (objStr == null || objStr.isEmpty()) return new CurrencyValue(null, "GBP");
Double amount = extractJsonDouble(objStr, "amount");
String currency = extractJsonString(objStr, "currency");
if (currency == null) currency = "GBP";
return new CurrencyValue(amount, currency);
}
private static String extractJsonString(String json, String key) {
String pattern = "\"" + key + "\":\"";
int idx = json.indexOf(pattern);
if (idx == -1) return null;
int start = idx + pattern.length();
int end = json.indexOf("\"", start);
if (end == -1) return null;
return json.substring(start, end).replace("\\\"", "\"");
}
private static Double extractJsonDouble(String json, String key) {
String pattern = "\"" + key + "\":";
int idx = json.indexOf(pattern);
if (idx == -1) return null;
int start = idx + pattern.length();
int end = start;
while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '.' || json.charAt(end) == '-')) {
end++;
}
if (start == end) return null;
try {
return Double.parseDouble(json.substring(start, end));
} catch (NumberFormatException e) {
return null;
}
}
private static Integer extractJsonNumber(String json, String key) {
Double d = extractJsonDouble(json, key);
return d != null ? d.intValue() : null;
}
private static String extractJsonArray(String json, String key) {
String pattern = "\"" + key + "\":[";
int idx = json.indexOf(pattern);
if (idx == -1) return "";
int start = idx + pattern.length() - 1;
int depth = 0;
int end = start;
for (int i = start; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '[' || c == '{') depth++;
else if (c == ']' || c == '}') depth--;
if (depth == 0 && c == ']') {
end = i + 1;
break;
}
}
return json.substring(start, end);
}
private static String extractJsonObject(String json, String key) {
String pattern = "\"" + key + "\":{";
int idx = json.indexOf(pattern);
if (idx == -1) {
pattern = "\"" + key + "\":null";
if (json.indexOf(pattern) != -1) return "";
return null;
}
int start = idx + pattern.length() - 1;
int depth = 0;
int end = start;
for (int i = start; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '{') depth++;
else if (c == '}') depth--;
if (depth == 0 && c == '}') {
end = i + 1;
break;
}
}
return json.substring(start, end);
}
private static List<String> splitJsonArray(String arrayStr) {
List<String> items = new ArrayList<>();
int depth = 0;
int start = 1;
for (int i = 1; i < arrayStr.length() - 1; i++) {
char c = arrayStr.charAt(i);
if (c == '{' || c == '[') depth++;
else if (c == '}' || c == ']') depth--;
else if (c == ',' && depth == 0) {
items.add(arrayStr.substring(start, i).trim());
start = i + 1;
}
}
if (start < arrayStr.length() - 1) {
items.add(arrayStr.substring(start, arrayStr.length() - 1).trim());
}
return items;
}
public static void processReceipt(String filePath) throws IOException, InterruptedException {
String dataUrl = fileToDataUrl(filePath);
System.out.println("[1/2] Parsing receipt from " + filePath + "...");
String parseConfig = buildParseConfig(dataUrl);
String parseResponse = makeRequest("POST", "/v1/parseRuns", parseConfig);
String runId = extractJsonString(parseResponse, "id");
ParseRunResponse parseRun = pollParseRun(runId);
if (!"PROCESSED".equals(parseRun.status)) {
throw new IOException("Parse failed with status: " + parseRun.status);
}
StringBuilder receiptMarkdown = new StringBuilder();
for (int i = 0; i < parseRun.output.chunks.size(); i++) {
if (i > 0) receiptMarkdown.append("\n\n---\n\n");
receiptMarkdown.append(parseRun.output.chunks.get(i).content);
}
System.out.println("[1/2] ✓ Parse complete. Receipt length: " + receiptMarkdown.length() + " chars");
System.out.println("Sample:\n" + receiptMarkdown.substring(0, Math.min(300, receiptMarkdown.length())) + "...\n");
System.out.println("[2/2] Extracting structured fields...");
String extractConfig = buildExtractConfig(dataUrl);
String extractResponse = makeRequest("POST", "/v1/extractRuns", extractConfig);
String extractRunId = extractJsonString(extractResponse, "id");
ExtractRunResponse extractRun = pollExtractRun(extractRunId);
if (!"PROCESSED".equals(extractRun.status)) {
throw new IOException("Extract failed with status: " + extractRun.status);
}
Receipt receipt = extractRun.output.value;
System.out.println("[2/2] ✓ Extraction complete.\n");
System.out.println("=== RECEIPT SUMMARY ===");
System.out.println("Store: " + (receipt.store_name != null ? receipt.store_name : "N/A"));
System.out.println("Date/Time: " + (receipt.transaction_date != null ? receipt.transaction_date : "N/A") + " " + (receipt.transaction_time != null ? receipt.transaction_time : ""));
System.out.println("\nPayment Method: " + (receipt.payment_method != null ? receipt.payment_method : "N/A"));
if (receipt.card_type != null) {
System.out.println(" Card: " + receipt.card_type + " (***" + (receipt.card_last_four != null ? receipt.card_last_four : "****") + ")");
System.out.println(" Auth Code: " + (receipt.authorization_code != null ? receipt.authorization_code : "N/A"));
}
if (receipt.loyalty_program != null) {
System.out.println("\nLoyalty: " + receipt.loyalty_program);
System.out.println(" Points This Visit: " + (receipt.loyalty_points_earned != null ? receipt.loyalty_points_earned : 0));
System.out.println(" Total Points: " + (receipt.loyalty_points_total != null ? receipt.loyalty_points_total : 0));
}
System.out.println("\n=== ITEMS (" + receipt.line_items.size() + ") ===");
for (LineItem item : receipt.line_items) {
String qty = item.quantity != null ? item.quantity + " × " : "";
String unitPrice = item.unit_price != null && item.unit_price.amount != null
? "£" + String.format("%.2f", item.unit_price.amount)
: "";
String lineTotal = item.line_total != null && item.line_total.amount != null
? "£" + String.format("%.2f", item.line_total.amount)
: "—";
System.out.println(" " + (item.description != null ? item.description : "Unknown") + " " + qty + unitPrice + " = " + lineTotal);
}
System.out.println("\n=== TOTALS ===");
System.out.println("Subtotal: £" + (receipt.subtotal != null && receipt.subtotal.amount != null
? String.format("%.2f", receipt.subtotal.amount)
: "0.00"));
System.out.println("Tax: £" + (receipt.tax_amount != null && receipt.tax_amount.amount != null
? String.format("%.2f", receipt.tax_amount.amount)
: "0.00"));
System.out.println("TOTAL: £" + (receipt.total_amount != null && receipt.total_amount.amount != null
? String.format("%.2f", receipt.total_amount.amount)
: "0.00"));
if (receipt.change_due != null && receipt.change_due.amount != null) {
System.out.println("Change: £" + String.format("%.2f", receipt.change_due.amount));
}
}
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Usage: java ReceiptProcessor <receipt-image-path>");
System.exit(1);
}
try {
processReceipt(args[0]);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It calls https://api.extend.ai endpoints with standard net/http and encoding/json.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"time"
)
const extendAPIBase = "https://api.extend.ai"
type Client struct {
token string
}
func NewClient(token string) *Client {
return &Client{token: token}
}
func (c *Client) do(method, path string, body interface{}) ([]byte, error) {
url := extendAPIBase + path
var reqBody io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewReader(data)
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
}
return respBody, nil
}
type ParseRunRequest struct {
File FileInput `json:"file"`
Config ParseConfig `json:"config"`
}
type FileInput struct {
URL string `json:"url"`
}
type ParseConfig struct {
Mode string `json:"mode"`
OutputType string `json:"outputType"`
BlockOptions BlockOptions `json:"blockOptions"`
ChunkingStrategy ChunkingStrategy `json:"chunkingStrategy"`
}
type BlockOptions struct {
Text TextBlock `json:"text"`
Tables TablesBlock `json:"tables"`
Figures FiguresBlock `json:"figures"`
}
type TextBlock struct {
Agentic AgenticConfig `json:"agentic"`
SignatureDetectionEnabled bool `json:"signatureDetectionEnabled"`
}
type TablesBlock struct {
Agentic AgenticConfig `json:"agentic"`
TableHeaderContinuationEnabled bool `json:"tableHeaderContinuationEnabled"`
}
type FiguresBlock struct {
Enabled bool `json:"enabled"`
}
type AgenticConfig struct {
Enabled bool `json:"enabled"`
}
type ChunkingStrategy struct {
Type string `json:"type"`
}
type ParseRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
} `json:"output"`
}
type ExtractRunRequest struct {
File FileInput `json:"file"`
Config ExtractConfig `json:"config"`
}
type ExtractConfig struct {
Schema map[string]interface{} `json:"schema"`
}
type ExtractRunResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Output struct {
Value ReceiptData `json:"value"`
} `json:"output"`
}
type ReceiptData struct {
StoreName *string `json:"store_name"`
StoreLocation *string `json:"store_location"`
StorePhone *string `json:"store_phone"`
TransactionDate *string `json:"transaction_date"`
TransactionTime *string `json:"transaction_time"`
Items []LineItem `json:"items"`
Subtotal *float64 `json:"subtotal"`
Tax *float64 `json:"tax"`
Total *float64 `json:"total"`
PaymentMethod *string `json:"payment_method"`
ApprovalCode *string `json:"approval_code"`
TransactionReference *string `json:"transaction_reference"`
}
type LineItem struct {
Description *string `json:"description"`
Quantity *float64 `json:"quantity"`
UnitPrice *float64 `json:"unit_price"`
}
func (c *Client) createAndPollParseRun(req *ParseRunRequest) (*ParseRunResponse, error) {
data, err := c.do("POST", "/v1/parse-runs", req)
if err != nil {
return nil, err
}
var resp ParseRunResponse
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
runID := resp.ID
for {
data, err := c.do("GET", "/v1/parse-runs/"+runID, nil)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
if resp.Status == "PROCESSED" || resp.Status == "FAILED" {
return &resp, nil
}
time.Sleep(2 * time.Second)
}
}
func (c *Client) createAndPollExtractRun(req *ExtractRunRequest) (*ExtractRunResponse, error) {
data, err := c.do("POST", "/v1/extract-runs", req)
if err != nil {
return nil, err
}
var resp ExtractRunResponse
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
runID := resp.ID
for {
data, err := c.do("GET", "/v1/extract-runs/"+runID, nil)
if err != nil {
return nil, err
}
if err := json.Unmarshal(data, &resp); err != nil {
return nil, err
}
if resp.Status == "PROCESSED" || resp.Status == "FAILED" {
return &resp, nil
}
time.Sleep(2 * time.Second)
}
}
func processReceipt(filePath string) (map[string]interface{}, error) {
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("EXTEND_API_KEY environment variable not set")
}
client := NewClient(apiKey)
fileBuffer, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
dataURL := "data:application/octet-stream;base64," + base64.StdEncoding.EncodeToString(fileBuffer)
fmt.Printf("[1/2] Parsing receipt from %s...\n", filePath)
parseReq := &ParseRunRequest{
File: FileInput{URL: dataURL},
Config: ParseConfig{
Mode: "agentic_ocr",
OutputType: "markdown",
BlockOptions: BlockOptions{
Text: TextBlock{
Agentic: AgenticConfig{Enabled: true},
SignatureDetectionEnabled: true,
},
Tables: TablesBlock{
Agentic: AgenticConfig{Enabled: true},
TableHeaderContinuationEnabled: true,
},
Figures: FiguresBlock{Enabled: true},
},
ChunkingStrategy: ChunkingStrategy{Type: "page"},
},
}
parseRun, err := client.createAndPollParseRun(parseReq)
if err != nil {
return nil, err
}
if parseRun.Status != "PROCESSED" {
return nil, fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
var receiptMarkdown string
for i, chunk := range parseRun.Output.Chunks {
if i > 0 {
receiptMarkdown += "\n\n---\n\n"
}
receiptMarkdown += chunk.Content
}
fmt.Printf("[1/2] ✓ Parse complete. Receipt length: %d chars\n", len(receiptMarkdown))
if len(receiptMarkdown) > 300 {
fmt.Printf("Sample:\n%s...\n\n", receiptMarkdown[:300])
}
fmt.Println("[2/2] Extracting structured fields...")
receiptSchema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"store_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Name of the retail store",
},
"store_location": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Store address including street, city, state, and zip code",
},
"store_phone": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Store contact phone number",
},
"transaction_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Date of purchase in MM/DD/YY format",
},
"transaction_time": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Time of purchase in HH:MM:SS format",
},
"items": map[string]interface{}{
"type": "array",
"description": "List of purchased items",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"description": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Item name or description",
},
"quantity": map[string]interface{}{
"type": []string{"number", "null"},
"description": "Quantity of items purchased",
},
"unit_price": map[string]interface{}{
"type": []string{"number", "null"},
"description": "Price per unit",
},
},
},
},
"subtotal": map[string]interface{}{
"type": []string{"number", "null"},
"description": "Subtotal before tax",
},
"tax": map[string]interface{}{
"type": []string{"number", "null"},
"description": "Total tax amount",
},
"total": map[string]interface{}{
"type": []string{"number", "null"},
"description": "Final total amount due",
},
"payment_method": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Payment method used (e.g., Chase Visa)",
},
"approval_code": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Transaction approval code",
},
"transaction_reference": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Transaction reference or confirmation number",
},
},
}
extractReq := &ExtractRunRequest{
File: FileInput{URL: dataURL},
Config: ExtractConfig{
Schema: receiptSchema,
},
}
extractRun, err := client.createAndPollExtractRun(extractReq)
if err != nil {
return nil, err
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extract failed with status: %s", extractRun.Status)
}
receipt := extractRun.Output.Value
fmt.Println("[2/2] ✓ Extraction complete.\n")
fmt.Println("=== RECEIPT SUMMARY ===")
storeName := "N/A"
if receipt.StoreName != nil {
storeName = *receipt.StoreName
}
fmt.Printf("Store: %s\n", storeName)
dateTime := "N/A"
if receipt.TransactionDate != nil {
dateTime = *receipt.TransactionDate
if receipt.TransactionTime != nil {
dateTime += " " + *receipt.TransactionTime
}
}
fmt.Printf("Date/Time: %s\n", dateTime)
paymentMethod := "N/A"
if receipt.PaymentMethod != nil {
paymentMethod = *receipt.PaymentMethod
}
fmt.Printf("\nPayment Method: %s\n", paymentMethod)
if receipt.ApprovalCode != nil {
fmt.Printf(" Auth Code: %s\n", *receipt.ApprovalCode)
}
fmt.Printf("\n=== ITEMS (%d) ===\n", len(receipt.Items))
for _, item := range receipt.Items {
desc := "Unknown"
if item.Description != nil {
desc = *item.Description
}
qty := ""
if item.Quantity != nil {
qty = fmt.Sprintf("%.0f × ", *item.Quantity)
}
unitPrice := ""
if item.UnitPrice != nil {
unitPrice = fmt.Sprintf("£%.2f", *item.UnitPrice)
}
fmt.Printf(" %s %s%s\n", desc, qty, unitPrice)
}
fmt.Println("\n=== TOTALS ===")
subtotal := "0.00"
if receipt.Subtotal != nil {
subtotal = fmt.Sprintf("%.2f", *receipt.Subtotal)
}
fmt.Printf("Subtotal: £%s\n", subtotal)
tax := "0.00"
if receipt.Tax != nil {
tax = fmt.Sprintf("%.2f", *receipt.Tax)
}
fmt.Printf("Tax: £%s\n", tax)
total := "0.00"
if receipt.Total != nil {
total = fmt.Sprintf("%.2f", *receipt.Total)
}
fmt.Printf("TOTAL: £%s\n", total)
return map[string]interface{}{
"status": "success",
"markdown": receiptMarkdown,
"receipt": receipt,
}, nil
}
func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: %s <receipt-image-path>\n", os.Args[0])
os.Exit(1)
}
result, err := processReceipt(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
data, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(data))
}// Deploy the "Receipt" 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-parser.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-parser).
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-parser.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 Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
},
"signatureDetectionEnabled": true
},
"tables": {
"agentic": {
"enabled": true
},
"tableHeaderContinuationEnabled": true
},
"figures": {
"enabled": true
}
},
"chunkingStrategy": {
"type": "page",
"options": {}
}
}
}
}
]
};
async function main() {
console.log(`Deploying "${WORKFLOW.name}"…`);
if (state.workflowId) {
console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
} else {
// Reuse an existing workflow with the same name if one exists (e.g. a
// previous run's state file was lost) instead of creating a duplicate.
try {
const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
const existing = items.find((x) => x.name === WORKFLOW.name);
if (existing?.id) {
state.workflowId = existing.id; saveState();
console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
}
} catch { /* lookup is best-effort; fall through to create */ }
if (!state.workflowId) {
const created = await api("POST", "/workflows", WORKFLOW);
const wfId = created.id ?? created.workflow?.id;
if (!wfId) throw new Error("Could not read created workflow id from response");
state.workflowId = wfId; saveState();
console.log(`+ created workflow (${wfId})`);
}
}
// Deploy the current draft as a new version so the workflow is runnable —
// best-effort: some accounts/plans may not require this explicit step.
await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});
console.log("\nDone. Run documents through it with:");
console.log(` POST ${API}/workflow_runs { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}
main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from extend_ai import Extend
API_KEY = os.environ.get("EXTEND_API_KEY")
if not API_KEY:
print("Set EXTEND_API_KEY first.", file=sys.stderr)
sys.exit(1)
STATE_DIR = Path.cwd() / ".extend"
STATE_FILE = STATE_DIR / "receipt-parser.json"
state = {}
if STATE_FILE.exists():
state = json.loads(STATE_FILE.read_text())
def save_state():
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2))
# Workflow definition — extractor/classifier/splitter configs inline
WORKFLOW = {
"name": "Receipt Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": True
},
"signatureDetectionEnabled": True
},
"tables": {
"agentic": {
"enabled": True
},
"tableHeaderContinuationEnabled": True
},
"figures": {
"enabled": True
}
},
"chunkingStrategy": {
"type": "page",
"options": {}
}
}
}
}
]
}
def main():
client = Extend(token=API_KEY)
print(f'Deploying "{WORKFLOW["name"]}…"')
if state.get("workflowId"):
workflow_id = state["workflowId"]
print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
client.workflows.update(id=workflow_id, steps=WORKFLOW["steps"])
else:
# Reuse an existing workflow with the same name if one exists
existing_id = None
try:
workflows = client.workflows.list(name=WORKFLOW["name"])
items = workflows.data if hasattr(workflows, "data") else (workflows.items if hasattr(workflows, "items") else [])
for item in items:
if item.get("name") == WORKFLOW["name"]:
existing_id = item.get("id")
break
if 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:
# lookup is best-effort; fall through to create
pass
if not state.get("workflowId"):
created = client.workflows.create(**WORKFLOW)
workflow_id = created.id if hasattr(created, "id") else (created.workflow.id if hasattr(created, "workflow") else None)
if not workflow_id:
raise ValueError("Could not read created workflow id from response")
state["workflowId"] = workflow_id
save_state()
print(f"+ created workflow ({workflow_id})")
# Deploy the current draft as a new version so the workflow is runnable
try:
client.workflows.create_version(id=state["workflowId"])
except Exception:
# best-effort: some accounts/plans may not require this explicit step
pass
print("\nDone. Run documents through it with:")
print(f' POST https://api.extend.ai/workflow_runs {{ "workflow": {{ "id": "{state["workflowId"]}" }}, "file": {{ "url": "https://…" }} }}')
print("Or open the workflow in the Extend dashboard to review and deploy it.")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)// This code calls Extend's REST API directly using only Java's built-in
// java.net.http.HttpClient, because Extend has no official Java SDK yet.
// The API calls mirror the exact endpoints and JSON shapes the TypeScript
// reference SDK uses.
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ReceiptProvision {
private static final String API = "https://api.extend.ai";
private static final String VERSION = "2026-02-09";
private static final String API_KEY = System.getenv("EXTEND_API_KEY");
private static final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
private static final Path STATE_FILE = STATE_DIR.resolve("receipt-parser.json");
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
static class State {
String workflowId;
}
private static State state = new State();
public static void main(String[] args) throws Exception {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
loadState();
Map<String, Object> workflow = buildWorkflow();
String workflowName = (String) workflow.get("name");
System.out.println("Deploying \"" + workflowName + "\"…");
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
List<Map<String, Object>> steps = (List<Map<String, Object>>) workflow.get("steps");
api("POST", "/workflows/" + state.workflowId, Map.of("steps", steps));
} else {
try {
String encodedName = URLEncoder.encode(workflowName, StandardCharsets.UTF_8);
Map<String, Object> listResponse = api("GET", "/workflows?name=" + encodedName, null);
List<Map<String, Object>> items = (List<Map<String, Object>>) listResponse.getOrDefault("data",
listResponse.getOrDefault("items", List.of()));
Map<String, Object> existing = null;
for (Map<String, Object> item : items) {
if (workflowName.equals(item.get("name"))) {
existing = item;
break;
}
}
if (existing != null && existing.get("id") != null) {
String existingId = (String) existing.get("id");
state.workflowId = existingId;
saveState();
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId
+ ") — updating steps");
List<Map<String, Object>> steps = (List<Map<String, Object>>) workflow.get("steps");
api("POST", "/workflows/" + existingId, Map.of("steps", steps));
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (state.workflowId == null || state.workflowId.isEmpty()) {
Map<String, Object> created = api("POST", "/workflows", workflow);
String wfId = (String) created.get("id");
if (wfId == null) {
Map<String, Object> workflowObj = (Map<String, Object>) created.get("workflow");
if (workflowObj != null) {
wfId = (String) workflowObj.get("id");
}
}
if (wfId == null) {
throw new Exception("Could not read created workflow id from response");
}
state.workflowId = wfId;
saveState();
System.out.println("+ created workflow (" + wfId + ")");
}
}
try {
api("POST", "/workflows/" + state.workflowId + "/versions", Map.of());
} catch (Exception e) {
// best-effort: some accounts/plans may not require this explicit step
}
System.out.println("\nDone. Run documents through it with:");
System.out.println(" POST " + API + "/workflow_runs { workflow: { id: \"" + state.workflowId
+ "\" }, file: { url: \"https://…\" } }");
System.out.println("Or open the workflow in the Extend dashboard to review and deploy it.");
}
private static Map<String, Object> buildWorkflow() {
Map<String, Object> workflow = new LinkedHashMap<>();
workflow.put("name", "Receipt Processing Pipeline");
Map<String, Object> step1 = new LinkedHashMap<>();
step1.put("name", "startTrigger1");
step1.put("type", "TRIGGER");
step1.put("next", List.of(Map.of("step", "parse1")));
Map<String, Object> blockOptions = new LinkedHashMap<>();
Map<String, Object> textBlock = new LinkedHashMap<>();
textBlock.put("agentic", Map.of("enabled", true));
textBlock.put("signatureDetectionEnabled", true);
blockOptions.put("text", textBlock);
Map<String, Object> tablesBlock = new LinkedHashMap<>();
tablesBlock.put("agentic", Map.of("enabled", true));
tablesBlock.put("tableHeaderContinuationEnabled", true);
blockOptions.put("tables", tablesBlock);
blockOptions.put("figures", Map.of("enabled", true));
Map<String, Object> parseConfig = new LinkedHashMap<>();
parseConfig.put("blockOptions", blockOptions);
parseConfig.put("chunkingStrategy", Map.of("type", "page", "options", Map.of()));
Map<String, Object> config = new LinkedHashMap<>();
config.put("parseConfig", parseConfig);
Map<String, Object> step2 = new LinkedHashMap<>();
step2.put("name", "parse1");
step2.put("type", "PARSE");
step2.put("config", config);
workflow.put("steps", List.of(step1, step2));
return workflow;
}
private static Map<String, Object> api(String method, String pathName, Map<String, Object> body)
throws Exception {
String url = API + pathName;
String bodyJson = body != null ? toJson(body) : null;
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(url))
.header("Authorization", "Bearer " + API_KEY).header("x-extend-api-version", VERSION);
if (method.equals("POST")) {
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(bodyJson != null ? bodyJson : "{}"));
requestBuilder.header("Content-Type", "application/json");
} else if (method.equals("GET")) {
requestBuilder.GET();
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
Map<String, Object> data = parseJson(response.body());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
String errorMsg = toJson(data);
if (errorMsg.length() > 300) {
errorMsg = errorMsg.substring(0, 300);
}
throw new Exception(method + " " + pathName + " failed (" + response.statusCode() + "): " + errorMsg);
}
return data;
}
private static void loadState() throws IOException {
if (Files.exists(STATE_FILE)) {
String content = Files.readString(STATE_FILE);
Map<String, Object> parsed = parseJson(content);
if (parsed.containsKey("workflowId")) {
state.workflowId = (String) parsed.get("workflowId");
}
}
}
private static void saveState() throws IOException {
Files.createDirectories(STATE_DIR);
Map<String, Object> stateMap = new LinkedHashMap<>();
if (state.workflowId != null) {
stateMap.put("workflowId", state.workflowId);
}
Files.writeString(STATE_FILE, toJson(stateMap));
}
private static String toJson(Object obj) {
if (obj == null) {
return "null";
}
if (obj instanceof String) {
return "\"" + escapeJson((String) obj) + "\"";
}
if (obj instanceof Number || obj instanceof Boolean) {
return obj.toString();
}
if (obj instanceof Map) {
Map<String, Object> map = (Map<String, Object>) obj;
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first)
sb.append(",");
sb.append("\"").append(escapeJson(entry.getKey())).append("\":");
sb.append(toJson(entry.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
if (obj instanceof List) {
List<Object> list = (List<Object>) obj;
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : list) {
if (!first)
sb.append(",");
sb.append(toJson(item));
first = false;
}
sb.append("]");
return sb.toString();
}
return "null";
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r")
.replace("\t", "\\t");
}
private static Map<String, Object> parseJson(String json) {
json = json.trim();
if (json.isEmpty() || json.equals("{}")) {
return new HashMap<>();
}
return parseJsonObject(json, new int[] { 0 });
}
private static Map<String, Object> parseJsonObject(String json, int[] pos) {
Map<String, Object> map = new LinkedHashMap<>();
skipWhitespace(json, pos);
if (pos[0] >= json.length() || json.charAt(pos[0]) != '{') {
return map;
}
pos[0]++;
skipWhitespace(json, pos);
while (pos[0] < json.length() && json.charAt(pos[0]) != '}') {
skipWhitespace(json, pos);
String key = parseJsonString(json, pos);
skipWhitespace(json, pos);
if (pos[0] < json.length() && json.charAt(pos[0]) == ':') {
pos[0]++;
}
skipWhitespace(json, pos);
Object value = parseJsonValue(json, pos);
map.put(key, value);
skipWhitespace(json, pos);
if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
pos[0]++;
}
skipWhitespace(json, pos);
}
if (pos[0] < json.length() && json.charAt(pos[0]) == '}') {
pos[0]++;
}
return map;
}
private static List<Object> parseJsonArray(String json, int[] pos) {
List<Object> list = new java.util.ArrayList<>();
skipWhitespace(json, pos);
if (pos[0] >= json.length() || json.charAt(pos[0]) != '[') {
return list;
}
pos[0]++;
skipWhitespace(json, pos);
while (pos[0] < json.length() && json.charAt(pos[0]) != ']') {
Object value = parseJsonValue(json, pos);
list.add(value);
skipWhitespace(json, pos);
if (pos[0] < json.length() && json.charAt(pos[0]) == ',') {
pos[0]++;
}
skipWhitespace(json, pos);
}
if (pos[0] < json.length() && json.charAt(pos[0]) == ']') {
pos[0]++;
}
return list;
}
private static Object parseJsonValue(String json, int[] pos) {
skipWhitespace(json, pos);
if (pos[0] >= json.length()) {
return null;
}
char c = json.charAt(pos[0]);
if (c == '{') {
return parseJsonObject(json, pos);
}
if (c == '[') {
return parseJsonArray(json, pos);
}
if (c == '"') {
return parseJsonString(json, pos);
}
if (c == 't' || c == 'f') {
if (json.startsWith("true", pos[0])) {
pos[0] += 4;
return true;
}
if (json.startsWith("false", pos[0])) {
pos[0] += 5;
return false;
}
}
if (c == 'n') {
if (json.startsWith("null", pos[0])) {
pos[0] += 4;
return null;
}
}
if (c == '-' || Character.isDigit(c)) {
int start = pos[0];
if (c == '-')
pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) {
pos[0]++;
}
if (pos[0] < json.length() && json.charAt(pos[0]) == '.') {
pos[0]++;
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) {
pos[0]++;
}
}
if (pos[0] < json.length() && (json.charAt(pos[0]) == 'e' || json.charAt(pos[0]) == 'E')) {
pos[0]++;
if (pos[0] < json.length() && (json.charAt(pos[0]) == '+' || json.charAt(pos[0]) == '-')) {
pos[0]++;
}
while (pos[0] < json.length() && Character.isDigit(json.charAt(pos[0]))) {
pos[0]++;
}
}
String numStr = json.substring(start, pos[0]);
try {
if (numStr.contains(".") || numStr.contains("e") || numStr.contains("E")) {
return Double.parseDouble(numStr);
} else {
return Long.parseLong(numStr);
}
} catch (NumberFormatException e) {
return numStr;
}
}
return null;
}
private static String parseJsonString(String json, int[] pos) {
skipWhitespace(json, pos);
if (pos[0] >= json.length() || json.charAt(pos[0]) != '"') {
return "";
}
pos[0]++;
StringBuilder sb = new StringBuilder();
while (pos[0] < json.length() && json.charAt(pos[0]) != '"') {
char c = json.charAt(pos[0]);
if (c == '\\' && pos[0] + 1 < json.length()) {
pos[0]++;
char escaped = json.charAt(pos[0]);
switch (escaped) {
case '"':
sb.append('"');
break;
case '\\':
sb.append('\\');
break;
case '/':
sb.append('/');
break;
case 'b':
sb.append('\b');
break;
case 'f':
sb.append('\f');
break;
case 'n':
sb.append('\n');
break;
case 'r':
sb.append('\r');
break;
case 't':
sb.append('\t');
break;
case 'u':
if (pos[0] + 4 < json.length()) {
String hex = json.substring(pos[0] + 1, pos[0] + 5);
sb.append((char) Integer.parseInt(hex, 16));
pos[0] += 4;
}
break;
default:
sb.append(escaped);
}
} else {
sb.append(c);
}
pos[0]++;
}
if (pos[0] < json.length() && json.charAt(pos[0]) == '"') {
pos[0]++;
}
return sb.toString();
}
private static void skipWhitespace(String json, int[] pos) {
while (pos[0] < json.length() && Character.isWhitespace(json.charAt(pos[0]))) {
pos[0]++;
}
}
}// This code uses the Extend REST API directly because Extend has no official Go SDK yet.
// It deploys the "Receipt" 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-parser).
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
type WorkflowStep struct {
Name string `json:"name"`
Type string `json:"type"`
Next []interface{} `json:"next,omitempty"`
Config interface{} `json:"config,omitempty"`
}
type Workflow struct {
Name string `json:"name"`
Steps []WorkflowStep `json:"steps"`
}
type WorkflowResponse struct {
ID string `json:"id,omitempty"`
Workflow struct {
ID string `json:"id,omitempty"`
} `json:"workflow,omitempty"`
}
type WorkflowListResponse struct {
Data []WorkflowItem `json:"data,omitempty"`
Items []WorkflowItem `json:"items,omitempty"`
}
type WorkflowItem struct {
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
}
var (
apiKey string
stateDir string
stateFile string
state State
)
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
stateDir = filepath.Join(".", ".extend")
stateFile = filepath.Join(stateDir, "receipt-parser.json")
// Load existing state if it exists
if data, err := os.ReadFile(stateFile); err == nil {
json.Unmarshal(data, &state)
}
}
func saveState() error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (map[string]interface{}, error) {
var reqBody io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequest(method, API+pathName, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var data map[string]interface{}
json.Unmarshal(respBody, &data)
if resp.StatusCode >= 400 {
respStr := string(respBody)
if len(respStr) > 300 {
respStr = respStr[:300]
}
return nil, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, respStr)
}
return data, nil
}
func main() {
workflow := Workflow{
Name: "Receipt Processing Pipeline",
Steps: []WorkflowStep{
{
Name: "startTrigger1",
Type: "TRIGGER",
Next: []interface{}{
map[string]string{"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]bool{
"enabled": true,
},
"signatureDetectionEnabled": true,
},
"tables": map[string]interface{}{
"agentic": map[string]bool{
"enabled": true,
},
"tableHeaderContinuationEnabled": true,
},
"figures": map[string]interface{}{
"enabled": true,
},
},
"chunkingStrategy": map[string]interface{}{
"type": "page",
"options": map[string]interface{}{},
},
},
},
},
},
}
fmt.Printf("Deploying \"%s\"…\n", workflow.Name)
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", state.WorkflowID), map[string]interface{}{"steps": workflow.Steps})
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
} else {
// Try to find existing workflow with same name
query := url.QueryEscape(workflow.Name)
listResp, err := apiCall("GET", fmt.Sprintf("/workflows?name=%s", query), nil)
if err == nil {
var items []WorkflowItem
if data, ok := listResp["data"].([]interface{}); ok {
for _, item := range data {
if itemMap, ok := item.(map[string]interface{}); ok {
var wi WorkflowItem
itemBytes, _ := json.Marshal(itemMap)
json.Unmarshal(itemBytes, &wi)
items = append(items, wi)
}
}
} else if data, ok := listResp["items"].([]interface{}); ok {
for _, item := range data {
if itemMap, ok := item.(map[string]interface{}); ok {
var wi WorkflowItem
itemBytes, _ := json.Marshal(itemMap)
json.Unmarshal(itemBytes, &wi)
items = append(items, wi)
}
}
}
for _, item := range items {
if item.Name == workflow.Name && item.ID != "" {
state.WorkflowID = item.ID
saveState()
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, item.ID)
_, err := apiCall("POST", fmt.Sprintf("/workflows/%s", item.ID), map[string]interface{}{"steps": workflow.Steps})
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
break
}
}
}
if state.WorkflowID == "" {
created, err := apiCall("POST", "/workflows", workflow)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
wfID := ""
if id, ok := created["id"].(string); ok {
wfID = id
} else if wfObj, ok := created["workflow"].(map[string]interface{}); ok {
if id, ok := wfObj["id"].(string); ok {
wfID = id
}
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
saveState()
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
// Deploy the current draft as a new version (best-effort)
apiCall("POST", fmt.Sprintf("/workflows/%s/versions", state.WorkflowID), map[string]interface{}{})
fmt.Printf("\nDone. Run documents through it with:\n")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Printf("Or open the workflow in the Extend dashboard to review and deploy it.\n")
}This template processes retail receipts to capture transaction details including store information, itemized products with prices, subtotals, taxes, and payment method confirmations. It handles variable-length item lists and payment authorization codes commonly found in point-of-sale receipts.