Extracts charges, billing details, line items, totals, and due dates vendor invoices.
A shipping invoice is a billing document issued by a freight or logistics provider that itemizes charges for cargo transportation services, including vessel details, port information, customer billing data, and payment terms. This template takes in Shipping Invoice and outputs markdown (.md) capturing the invoice's full text and layout, and JSON (.json) with structured billing and shipping fields including invoice identifiers, dates, party information, port details, vessel voyage data, and amount due per the extraction schema by using Extend's Parse, Extract primitives.
Converts the document into clean, layout-aware markdown plus structured blocks with spatial metadata.
blockOptions.text.agentic.enabledtruechangedchunkingStrategy.type"document"engine"parse_performance"You can learn more about Parse configuration in Extend's Parse documentation.
Pulls a defined set of fields from the document and returns them as structured JSON matching a schema.
metadata.bill_of_lading_number.citations[{"page":1,"polygon":[{"x":27.8784,"y":470.3544},{"x":321.4296,"y":470.3328},{"x":321.4368,"y":481.9464},{"x":27.885599999999997,"y":481.968}],"pageWidth":595.0…changedmetadata.bill_of_lading_number.logprobsConfidencenullchangedmetadata.bill_of_lading_number.ocrConfidence0.986changedmetadata.container_details.citations[]changedmetadata.container_details.logprobsConfidencenullchangedmetadata.container_details.ocrConfidence0.734changedmetadata.container_details[0].citations[{"page":2,"polygon":[{"x":28.7352,"y":130.66559999999998},{"x":33.3936,"y":130.6872},{"x":33.372,"y":138.2472},{"x":28.7064,"y":138.2328}],"pageWidth":595.0008…changedmetadata.container_details[0].container_number.logprobsConfidencenullchangedmetadata.container_details[0].container_number.ocrConfidence0.993changedmetadata.container_details[0].logprobsConfidencenullchangedmetadata.container_details[0].ocrConfidence0.734changedmetadata.container_details[0].pcd.logprobsConfidencenullchangedmetadata.container_details[0].pcd.ocrConfidence0.992changedmetadata.container_details[0].service.logprobsConfidencenullchangedmetadata.container_details[0].service.ocrConfidence0.995changedmetadata.container_details[0].size_type.logprobsConfidencenullchangedmetadata.container_details[0].size_type.ocrConfidence0.734changedmetadata.container_details[1].citations[{"page":2,"polygon":[{"x":28.0872,"y":143.23680000000002},{"x":33.955200000000005,"y":143.2584},{"x":33.9264,"y":151.30800000000002},{"x":28.0512,"y":151.2864}…changedmetadata.container_details[1].container_number.logprobsConfidencenullchangedmetadata.container_details[1].container_number.ocrConfidence0.993changedmetadata.container_details[1].logprobsConfidencenullchangedmetadata.container_details[1].ocrConfidence0.734changedmetadata.container_details[1].pcd.logprobsConfidencenullchangedmetadata.container_details[1].pcd.ocrConfidence0.992changedmetadata.container_details[1].service.logprobsConfidencenullchangedmetadata.container_details[1].service.ocrConfidence0.995changedmetadata.container_details[1].size_type.logprobsConfidencenullchangedmetadata.container_details[1].size_type.ocrConfidence0.734changedmetadata.container_details[2].citations[{"page":2,"polygon":[{"x":28.108800000000002,"y":156.1968},{"x":33.8184,"y":156.2184},{"x":33.7896,"y":164.3328},{"x":28.080000000000002,"y":164.31119999999999…changedmetadata.container_details[2].container_number.logprobsConfidencenullchangedmetadata.container_details[2].container_number.ocrConfidence0.994changedmetadata.container_details[2].logprobsConfidencenullchangedmetadata.container_details[2].ocrConfidence0.734changedmetadata.container_details[2].pcd.logprobsConfidencenullchangedmetadata.container_details[2].pcd.ocrConfidence0.992changedmetadata.container_details[2].service.logprobsConfidencenullchangedmetadata.container_details[2].service.ocrConfidence0.995changedmetadata.container_details[2].size_type.logprobsConfidencenullchangedmetadata.container_details[2].size_type.ocrConfidence0.734changedmetadata.container_details[3].citations[{"page":2,"polygon":[{"x":28.2024,"y":169.704},{"x":33.8256,"y":169.74},{"x":33.7824,"y":177.12720000000002},{"x":28.1592,"y":177.09840000000003}],"pageWidth":…changedmetadata.container_details[3].container_number.logprobsConfidencenullchangedmetadata.container_details[3].container_number.ocrConfidence0.992changedmetadata.container_details[3].logprobsConfidencenullchangedmetadata.container_details[3].ocrConfidence0.734changedmetadata.container_details[3].pcd.logprobsConfidencenullchangedmetadata.container_details[3].pcd.ocrConfidence0.992changedmetadata.container_details[3].service.logprobsConfidencenullchangedmetadata.container_details[3].service.ocrConfidence0.995changedmetadata.container_details[3].size_type.logprobsConfidencenullchangedmetadata.container_details[3].size_type.ocrConfidence0.734changedmetadata.container_details[4].citations[{"page":2,"polygon":[{"x":28.1808,"y":182.7576},{"x":33.768,"y":182.7864},{"x":33.732,"y":190.59840000000003},{"x":28.144800000000004,"y":190.56959999999998}],…changedmetadata.container_details[4].container_number.logprobsConfidencenullchangedmetadata.container_details[4].container_number.ocrConfidence0.993changedmetadata.container_details[4].logprobsConfidencenullchangedmetadata.container_details[4].ocrConfidence0.734changedmetadata.container_details[4].pcd.logprobsConfidencenullchangedmetadata.container_details[4].pcd.ocrConfidence0.992changedmetadata.container_details[4].service.logprobsConfidencenullchangedmetadata.container_details[4].service.ocrConfidence0.995changedmetadata.container_details[4].size_type.logprobsConfidencenullchangedmetadata.container_details[4].size_type.ocrConfidence0.734changedmetadata.container_details[5].citations[{"page":2,"polygon":[{"x":28.26,"y":195.4008},{"x":33.7536,"y":195.42960000000002},{"x":33.7104,"y":203.3424},{"x":28.216800000000003,"y":203.31359999999998}],…changedmetadata.container_details[5].container_number.logprobsConfidencenullchangedmetadata.container_details[5].container_number.ocrConfidence0.994changedmetadata.container_details[5].logprobsConfidencenullchangedmetadata.container_details[5].ocrConfidence0.734changedmetadata.container_details[5].pcd.logprobsConfidencenullchangedmetadata.container_details[5].pcd.ocrConfidence0.992changedmetadata.container_details[5].service.logprobsConfidencenullchangedmetadata.container_details[5].service.ocrConfidence0.995changedmetadata.container_details[5].size_type.logprobsConfidencenullchangedmetadata.container_details[5].size_type.ocrConfidence0.734changedmetadata.container_details[6].citations[{"page":2,"polygon":[{"x":28.1592,"y":208.4184},{"x":33.580799999999996,"y":208.44719999999998},{"x":33.5376,"y":216.18},{"x":28.116,"y":216.1512}],"pageWidth"…changedmetadata.container_details[6].container_number.logprobsConfidencenullchangedmetadata.container_details[6].container_number.ocrConfidence0.995changedmetadata.container_details[6].logprobsConfidencenullchangedmetadata.container_details[6].ocrConfidence0.734changedmetadata.container_details[6].pcd.logprobsConfidencenullchangedmetadata.container_details[6].pcd.ocrConfidence0.992changedmetadata.container_details[6].service.logprobsConfidencenullchangedmetadata.container_details[6].service.ocrConfidence0.995changedmetadata.container_details[6].size_type.logprobsConfidencenullchangedmetadata.container_details[6].size_type.ocrConfidence0.734changedmetadata.customer_address.citations[{"page":1,"polygon":[{"x":117.2376,"y":141.2568},{"x":176.7816,"y":141.3432},{"x":176.7672,"y":150.4728},{"x":117.2232,"y":150.42239999999998}],"pageWidth":595…changedmetadata.customer_address.logprobsConfidencenullchangedmetadata.customer_address.ocrConfidence0.986changedmetadata.customer_name.citations[{"page":1,"polygon":[{"x":117.1224,"y":130.44240000000002},{"x":203.4864,"y":130.5072},{"x":203.4792,"y":139.8888},{"x":117.1152,"y":139.82399999999998}],"page…changedmetadata.customer_name.logprobsConfidencenullchangedmetadata.customer_name.ocrConfidence0.986changedmetadata.customer_number.citations[{"page":1,"polygon":[{"x":116.244,"y":258.0408},{"x":174.68640000000002,"y":258.0336},{"x":174.68640000000002,"y":267.2424},{"x":116.2512,"y":267.2496}],"pageW…changedmetadata.customer_number.logprobsConfidencenullchangedmetadata.customer_number.ocrConfidence0.986changedmetadata.customer_tax_id.citations[{"page":1,"polygon":[{"x":116.3952,"y":269.4816},{"x":216.16559999999998,"y":269.4816},{"x":216.16559999999998,"y":278.9064},{"x":116.3952,"y":278.9064}],"page…changedmetadata.customer_tax_id.logprobsConfidencenullchangedmetadata.customer_tax_id.ocrConfidence0.975changedmetadata.due_date.citations[{"page":1,"polygon":[{"x":451.7712,"y":141.5088},{"x":509.14079999999996,"y":141.552},{"x":509.1336,"y":151.2144},{"x":451.764,"y":151.1712}],"pageWidth":595.0…changedmetadata.due_date.logprobsConfidencenullchangedmetadata.due_date.ocrConfidence0.993changedmetadata.invoice_date.citations[{"page":1,"polygon":[{"x":451.692,"y":130.3776},{"x":509.11920000000003,"y":130.6368},{"x":509.076,"y":140.148},{"x":451.6488,"y":139.8888}],"pageWidth":595.00…changedmetadata.invoice_date.logprobsConfidencenullchangedmetadata.invoice_date.ocrConfidence0.992changedmetadata.invoice_number.citations[{"page":1,"polygon":[{"x":445.4928,"y":112.8168},{"x":543.2976,"y":112.85999999999999},{"x":543.2904,"y":125.8272},{"x":445.4928,"y":125.784}],"pageWidth":595.…changedmetadata.invoice_number.logprobsConfidencenullchangedmetadata.invoice_number.ocrConfidence0.992changedmetadata.line_items.citations[]changedmetadata.line_items.logprobsConfidencenullchangedmetadata.line_items.ocrConfidence0.934changedmetadata.line_items[0].citations[{"page":1,"polygon":[{"x":28.281599999999997,"y":548.5608},{"x":153.8496,"y":548.7696000000001},{"x":153.83520000000001,"y":559.3104000000001},{"x":28.26,"y":5…changedmetadata.line_items[0].currency.logprobsConfidencenullchangedmetadata.line_items[0].currency.ocrConfidence0.999changedmetadata.line_items[0].description.logprobsConfidencenullchangedmetadata.line_items[0].description.ocrConfidence0.983changedmetadata.line_items[0].exchange_rate.logprobsConfidencenullchangedmetadata.line_items[0].exchange_rate.ocrConfidence0.992changedmetadata.line_items[0].logprobsConfidencenullchangedmetadata.line_items[0].ocrConfidence0.945changedmetadata.line_items[0].quantity.logprobsConfidencenullchangedmetadata.line_items[0].quantity.ocrConfidence0.945changedmetadata.line_items[0].total_amount.logprobsConfidencenullchangedmetadata.line_items[0].total_amount.ocrConfidence0.991changedmetadata.line_items[0].unit_of_measure.logprobsConfidencenullchangedmetadata.line_items[0].unit_of_measure.ocrConfidence0.996changedmetadata.line_items[0].unit_price.logprobsConfidencenullchangedmetadata.line_items[0].unit_price.ocrConfidence0.991changedmetadata.line_items[1].citations[{"page":1,"polygon":[{"x":28.4544,"y":562.2048},{"x":176.4144,"y":562.1976},{"x":176.4144,"y":572.292},{"x":28.4544,"y":572.2992}],"pageWidth":595.0008,"pageHe…changedmetadata.line_items[1].currency.logprobsConfidencenullchangedmetadata.line_items[1].currency.ocrConfidence0.995changedmetadata.line_items[1].description.logprobsConfidencenullchangedmetadata.line_items[1].description.ocrConfidence0.977changedmetadata.line_items[1].exchange_rate.logprobsConfidencenullchangedmetadata.line_items[1].exchange_rate.ocrConfidence0.995changedmetadata.line_items[1].logprobsConfidencenullchangedmetadata.line_items[1].ocrConfidence0.934changedmetadata.line_items[1].quantity.logprobsConfidencenullchangedmetadata.line_items[1].quantity.ocrConfidence0.992changedmetadata.line_items[1].total_amount.logprobsConfidencenullchangedmetadata.line_items[1].total_amount.ocrConfidence0.995changedmetadata.line_items[1].unit_of_measure.logprobsConfidencenullchangedmetadata.line_items[1].unit_of_measure.ocrConfidence0.998changedmetadata.line_items[1].unit_price.logprobsConfidencenullchangedmetadata.line_items[1].unit_price.ocrConfidence0.934changedmetadata.line_items[2].citations[{"page":1,"polygon":[{"x":28.0152,"y":575.4024},{"x":166.032,"y":575.2872},{"x":166.0392,"y":586.08},{"x":28.022399999999998,"y":586.1952}],"pageWidth":595.000…changedmetadata.line_items[2].currency.logprobsConfidencenullchangedmetadata.line_items[2].currency.ocrConfidence0.995changedmetadata.line_items[2].description.logprobsConfidencenullchangedmetadata.line_items[2].description.ocrConfidence0.992changedmetadata.line_items[2].exchange_rate.logprobsConfidencenullchangedmetadata.line_items[2].exchange_rate.ocrConfidence0.995changedmetadata.line_items[2].logprobsConfidencenullchangedmetadata.line_items[2].ocrConfidence0.945changedmetadata.line_items[2].quantity.logprobsConfidencenullchangedmetadata.line_items[2].quantity.ocrConfidence0.945changedmetadata.line_items[2].total_amount.logprobsConfidencenullchangedmetadata.line_items[2].total_amount.ocrConfidence0.995changedmetadata.line_items[2].unit_of_measure.logprobsConfidencenullchangedmetadata.line_items[2].unit_of_measure.ocrConfidence0.996changedmetadata.line_items[2].unit_price.logprobsConfidencenullchangedmetadata.line_items[2].unit_price.ocrConfidence0.997changedmetadata.line_items[3].citations[{"page":1,"polygon":[{"x":27.9432,"y":588.3624},{"x":135.9072,"y":588.6792},{"x":135.8784,"y":598.9608},{"x":27.9072,"y":598.644}],"pageWidth":595.0008,"pageHe…changedmetadata.line_items[3].currency.logprobsConfidencenullchangedmetadata.line_items[3].currency.ocrConfidence0.995changedmetadata.line_items[3].description.logprobsConfidencenullchangedmetadata.line_items[3].description.ocrConfidence0.992changedmetadata.line_items[3].exchange_rate.logprobsConfidencenullchangedmetadata.line_items[3].exchange_rate.ocrConfidence0.995changedmetadata.line_items[3].logprobsConfidencenullchangedmetadata.line_items[3].ocrConfidence0.945changedmetadata.line_items[3].quantity.logprobsConfidencenullchangedmetadata.line_items[3].quantity.ocrConfidence0.945changedmetadata.line_items[3].total_amount.logprobsConfidencenullchangedmetadata.line_items[3].total_amount.ocrConfidence0.995changedmetadata.line_items[3].unit_of_measure.logprobsConfidencenullchangedmetadata.line_items[3].unit_of_measure.ocrConfidence0.996changedmetadata.line_items[3].unit_price.logprobsConfidencenullchangedmetadata.line_items[3].unit_price.ocrConfidence0.991changedmetadata.line_items[4].citations[{"page":1,"polygon":[{"x":27.928800000000003,"y":602.0064},{"x":154.5624,"y":602.0208},{"x":154.55519999999999,"y":612.288},{"x":27.928800000000003,"y":612.273…changedmetadata.line_items[4].currency.logprobsConfidencenullchangedmetadata.line_items[4].currency.ocrConfidence0.995changedmetadata.line_items[4].description.logprobsConfidencenullchangedmetadata.line_items[4].description.ocrConfidence0.977changedmetadata.line_items[4].exchange_rate.logprobsConfidencenullchangedmetadata.line_items[4].exchange_rate.ocrConfidence0.995changedmetadata.line_items[4].logprobsConfidencenullchangedmetadata.line_items[4].ocrConfidence0.945changedmetadata.line_items[4].quantity.logprobsConfidencenullchangedmetadata.line_items[4].quantity.ocrConfidence0.945changedmetadata.line_items[4].total_amount.logprobsConfidencenullchangedmetadata.line_items[4].total_amount.ocrConfidence0.993changedmetadata.line_items[4].unit_of_measure.logprobsConfidencenullchangedmetadata.line_items[4].unit_of_measure.ocrConfidence0.996changedmetadata.line_items[4].unit_price.logprobsConfidencenullchangedmetadata.line_items[4].unit_price.ocrConfidence0.992changedmetadata.payment_terms.citations[{"page":1,"polygon":[{"x":451.0512,"y":164.4552},{"x":545.9688,"y":164.8152},{"x":545.9256,"y":175.5864},{"x":451.00800000000004,"y":175.212}],"pageWidth":595.…changedmetadata.payment_terms.logprobsConfidencenullchangedmetadata.payment_terms.ocrConfidence0.992changedmetadata.reference_number.citations[]changedmetadata.reference_number.logprobsConfidencenullchangedmetadata.shipment_details.citations[]changedmetadata.shipment_details.eta.citations[{"page":1,"polygon":[{"x":169.4592,"y":518.5224},{"x":244.14479999999998,"y":518.5152},{"x":244.14479999999998,"y":527.4216},{"x":169.4592,"y":527.4288}],"page…changedmetadata.shipment_details.eta.logprobsConfidencenullchangedmetadata.shipment_details.eta.ocrConfidence0.987changedmetadata.shipment_details.etd.citations[{"page":1,"polygon":[{"x":27.6192,"y":518.4431999999999},{"x":105.1056,"y":518.5008},{"x":105.0984,"y":527.6016},{"x":27.612000000000002,"y":527.544}],"pageWid…changedmetadata.shipment_details.etd.logprobsConfidencenullchangedmetadata.shipment_details.etd.ocrConfidence0.989changedmetadata.shipment_details.logprobsConfidencenullchangedmetadata.shipment_details.ocrConfidence0.984changedmetadata.shipment_details.place_of_delivery.citations[{"page":1,"polygon":[{"x":169.29360000000003,"y":503.8488},{"x":287.5176,"y":504.1368},{"x":287.496,"y":513.936},{"x":169.2648,"y":513.648}],"pageWidth":595.00…changedmetadata.shipment_details.place_of_delivery.logprobsConfidencenullchangedmetadata.shipment_details.place_of_delivery.ocrConfidence0.986changedmetadata.shipment_details.place_of_receipt.citations[{"page":1,"polygon":[{"x":169.2288,"y":490.176},{"x":273.096,"y":490.6368},{"x":273.0528,"y":500.2416},{"x":169.18560000000002,"y":499.7808}],"pageWidth":595.0…changedmetadata.shipment_details.place_of_receipt.logprobsConfidencenullchangedmetadata.shipment_details.place_of_receipt.ocrConfidence0.984changedmetadata.shipment_details.port_of_discharge.citations[{"page":1,"polygon":[{"x":27.396,"y":503.91360000000003},{"x":96.06960000000001,"y":504.28799999999995},{"x":96.0192,"y":513.8928000000001},{"x":27.3456,"y":51…changedmetadata.shipment_details.port_of_discharge.logprobsConfidencenullchangedmetadata.shipment_details.port_of_discharge.ocrConfidence0.988changedmetadata.shipment_details.port_of_loading.citations[{"page":1,"polygon":[{"x":27.3888,"y":490.032},{"x":83.2608,"y":490.5648},{"x":83.16,"y":500.3496},{"x":27.2952,"y":499.8384}],"pageWidth":595.0008,"pageHeight…changedmetadata.shipment_details.port_of_loading.logprobsConfidencenullchangedmetadata.shipment_details.port_of_loading.ocrConfidence0.988changedmetadata.shipment_details.vessel_voyage.citations[{"page":1,"polygon":[{"x":339.0912,"y":490.1904},{"x":523.4688,"y":489.74399999999997},{"x":523.4904,"y":499.5432},{"x":339.12,"y":499.9392}],"pageWidth":595.0…changedmetadata.shipment_details.vessel_voyage.logprobsConfidencenullchangedmetadata.shipment_details.vessel_voyage.ocrConfidence0.992changedmetadata.tax_details.citations[]changedmetadata.tax_details.logprobsConfidencenullchangedmetadata.tax_details.ocrConfidence0.992changedmetadata.tax_details[0].citations[{"page":2,"polygon":[{"x":27.6768,"y":260.8128},{"x":89.856,"y":260.9064},{"x":89.8416,"y":270.58320000000003},{"x":27.662399999999998,"y":270.4896}],"pageWidt…changedmetadata.tax_details[0].logprobsConfidencenullchangedmetadata.tax_details[0].ocrConfidence0.992changedmetadata.tax_details[0].tax_amount.logprobsConfidencenullchangedmetadata.tax_details[0].tax_base_amount.logprobsConfidencenullchangedmetadata.tax_details[0].tax_base_amount.ocrConfidence0.993changedmetadata.tax_details[0].tax_rate_percent.logprobsConfidencenullchangedmetadata.tax_details[0].tax_rate_percent.ocrConfidence0.992changedmetadata.tax_details[0].tax_type.logprobsConfidencenullchangedmetadata.tax_details[0].tax_type.ocrConfidence0.995changedmetadata.tax_details[1].citations[{"page":2,"polygon":[{"x":27.9072,"y":274.05359999999996},{"x":102.2976,"y":274.2048},{"x":102.27600000000001,"y":283.7952},{"x":27.8928,"y":283.644}],"pageWid…changedmetadata.tax_details[1].logprobsConfidencenullchangedmetadata.tax_details[1].ocrConfidence0.992changedmetadata.tax_details[1].tax_amount.logprobsConfidencenullchangedmetadata.tax_details[1].tax_base_amount.logprobsConfidencenullchangedmetadata.tax_details[1].tax_base_amount.ocrConfidence0.993changedmetadata.tax_details[1].tax_rate_percent.logprobsConfidencenullchangedmetadata.tax_details[1].tax_type.logprobsConfidencenullchangedmetadata.tax_details[1].tax_type.ocrConfidence0.992changedmetadata.tax_details[2].citations[{"page":2,"polygon":[{"x":27.554399999999998,"y":287.6544},{"x":109.6488,"y":287.9424},{"x":109.6128,"y":297.48240000000004},{"x":27.5472,"y":297.1944}],"pageW…changedmetadata.tax_details[2].logprobsConfidencenullchangedmetadata.tax_details[2].ocrConfidence0.993changedmetadata.tax_details[2].tax_amount.logprobsConfidencenullchangedmetadata.tax_details[2].tax_base_amount.logprobsConfidencenullchangedmetadata.tax_details[2].tax_base_amount.ocrConfidence0.993changedmetadata.tax_details[2].tax_rate_percent.logprobsConfidencenullchangedmetadata.tax_details[2].tax_type.logprobsConfidencenullchangedmetadata.tax_details[2].tax_type.ocrConfidence0.995changedmetadata.total_amount_due.citations[{"page":1,"polygon":[{"x":27.7128,"y":436.24080000000004},{"x":97.7904,"y":436.1112},{"x":97.8048,"y":445.8816},{"x":27.7272,"y":446.01120000000003}],"pageWidt…changedmetadata.total_amount_due.logprobsConfidencenullchangedmetadata.total_amount_due.ocrConfidence0.997changedmetadata.vendor_address.citations[{"page":1,"polygon":[{"x":28.0872,"y":23.025599999999997},{"x":259.6248,"y":23.1912},{"x":259.6032,"y":31.9752},{"x":28.0656,"y":31.4496}],"pageWidth":595.0008…changedmetadata.vendor_address.logprobsConfidencenullchangedmetadata.vendor_address.ocrConfidence0.983changedmetadata.vendor_name.citations[{"page":1,"polygon":[{"x":27.705599999999997,"y":8.6832},{"x":147.80880000000002,"y":8.8344},{"x":147.8016,"y":17.1648},{"x":27.6984,"y":17.0136}],"pageWidth":…changedmetadata.vendor_name.logprobsConfidencenullchangedmetadata.vendor_name.ocrConfidence0.859changedmetadata.vendor_tax_id.citations[{"page":1,"polygon":[{"x":27.9864,"y":44.812799999999996},{"x":131.17680000000001,"y":44.7624},{"x":131.184,"y":52.9344},{"x":27.993599999999997,"y":52.9848}],…changedmetadata.vendor_tax_id.logprobsConfidencenullchangedmetadata.vendor_tax_id.ocrConfidence0.975changedvalue.bill_of_lading_number"920667821"changedvalue.container_details[{"pcd":"2020-01-16","service":"CY/CY","size_type":"40/9'6/DRY","container_number":"HASU4458596"},{"pcd":"2020-01-16","service":"CY/CY","size_type":"40/9'6/DRY"…changedvalue.customer_address"ROXAS BLVD, 34F PEARL OF THE ORIENT TOWER, 1000 Manila"changedvalue.customer_name"BELLREX TRADING"changedvalue.customer_number"PH00269154"changedvalue.customer_tax_id"938-438-810-00000X"changedvalue.due_date"2020-02-07"changedvalue.invoice_date"2020-02-02"changedvalue.invoice_number"5654198635"changedvalue.line_items[{"currency":"PHP","quantity":7,"unit_price":1000,"description":"Container Cleaning Imports","total_amount":7000,"exchange_rate":1,"unit_of_measure":"CNT"},{"cu…changedvalue.payment_terms"Payable immediately"changedvalue.reference_numbernullchangedvalue.shipment_details.eta"2020-02-07"changedvalue.shipment_details.etd"2020-01-16"changedvalue.shipment_details.place_of_delivery"Davao City"changedvalue.shipment_details.place_of_receipt"Xingang"changedvalue.shipment_details.port_of_discharge"Davao City"changedvalue.shipment_details.port_of_loading"Xingang"changedvalue.shipment_details.vessel_voyage"HAMMONIA BEROLINA/003S"changedvalue.tax_details[{"tax_type":"Vatable Sales","tax_amount":0,"tax_base_amount":325512.64,"tax_rate_percent":12},{"tax_type":"Zero rated sales","tax_amount":0,"tax_base_amount":3…changedvalue.total_amount_due.amount325512.64changedvalue.total_amount_due.iso_4217_currency_code"PHP"changedvalue.vendor_address"9/F One E-com Center, Harbor Drive Corner Sunset Ave., Mall of Asia Complex, 1300 Pasay City"changedvalue.vendor_name"Sealand Maersk Asia Pte. Ltd."changedvalue.vendor_tax_id"249-765-638-00000"changedYou can learn more about Extract configuration in Extend's Extract documentation.
{
"name": "Vendor Invoice Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT"
}
]
}# Vendor Invoice Processing — Extend AI Skill
## What this pipeline does
This pipeline converts vendor invoices (PDFs or scans) into structured, query-ready data. It parses the document to markdown for readability, then extracts key billing fields: vendor/customer details, invoice dates, payment terms, itemized line items with multi-currency support, tax breakdowns, shipment metadata, and container details. The output is a JSON object suitable for accounting systems, compliance audits, and payment workflows.
## When to use this
- **AP automation**: Feed invoices into accounts-payable systems for approval routing and payment scheduling.
- **Multi-currency reconciliation**: Handle mixed-currency line items with exchange rates and tax classifications (vatable, zero-rated, exempt).
- **Logistics tracking**: Extract container numbers, vessel/voyage, ports of loading/discharge, and ETA/ETD for import management.
- **Audit & compliance**: Capture tax IDs, tax bases, and tax rates for regulatory reporting and forensic analysis.
- **Scale processing**: Use async extraction for high-volume batches; sync parsing for real-time preview.
## Processor pipeline
| Step | Processor | Purpose | Config | Why |
|------|-----------|---------|--------|-----|
| 1 | **Parse** | Convert PDF to readable markdown + bounding boxes | `agentic_ocr: true`, document-level chunking | Invoices often have scans, logos, and mixed layouts; agentic mode handles these without manual templating. Document chunking keeps full invoice context. |
| 2 | **Extract** | Pull structured fields into JSON with multi-currency & tax arrays | Zod schema + `extendCurrency()`, nullable strings for optional fields, array items for line_items/tax_details/container_details | Zod enforces type safety and provides machine-readable descriptions for high accuracy. Multi-currency requires explicit schema fields (currency, exchange_rate). Arrays allow variable-length lists (invoices have 1–N line items). |
---
## TypeScript implementation
---
## CLI equivalent
```bash
# Step 1: Parse invoice to markdown
extend parse invoice.pdf
# Step 2: Extract using schema (save schema to file first)
cat > invoice_schema.json << 'EOF'
{
"vendor_name": "string | null",
"vendor_address": "string | null",
"vendor_tax_id": "string | null",
"customer_name": "string | null",
"customer_number": "string | null",
"customer_address": "string | null",
"customer_tax_id": "string | null",
"invoice_number": "string | null",
"invoice_date": "date (yyyy-mm-dd)",
"due_date": "date (yyyy-mm-dd)",
"payment_terms": "string | null",
"reference_number": "string | null",
"line_items": [
{
"description": "string | null",
"quantity": "number | null",
"unit_of_measure": "string | null",
"unit_price": "number | null",
"currency": "string | null",
"exchange_rate": "number | null",
"total_amount": "number | null"
}
],
"tax_details": [
{
"tax_type": "string | null",
"tax_rate_percent": "number | null",
"tax_base_amount": "number | null",
"tax_amount": "number | null"
}
],
"total_amount_due": { "amount": "number | null", "iso_4217_currency_code": "string | null" },
"bill_of_lading_number": "string | null",
"shipment_details": {
"etd": "date",
"eta": "date",
"port_of_loading": "string | null",
"place_of_receipt": "string | null",
"port_of_discharge": "string | null",
"place_of_delivery": "string | null",
"vessel_voyage": "string | null"
},
"container_details": [
{
"container_number": "string | null",
"size_type": "string | null",
"service": "string | null",
"pcd": "date"
}
]
}
EOF
extend extract invoice.pdf --schema invoice_schema.json
```
---
## Schema
The extraction schema is defined in the TypeScript implementation above using Zod. Key structure:
### Root object properties
| Field | Type | Nullable | Example | Why this matters |
|-------|------|----------|---------|-----------------|
| `vendor_name` | string | ✓ | "Sealand Maersk Asia Pte. Ltd." | Required for AR/matching with vendor master data; spelling errors break reconciliation. |
| `vendor_tax_id` | string | ✓ | "249-765-638-00000" | Critical for tax compliance and audit trail; format varies by jurisdiction. |
| `customer_name` | string | ✓ | "BELLREX TRADING" | Links invoice to buyer; enables AP workflow routing. |
| `customer_tax_id` | string | ✓ | "938-438-810-00000X" | Buyer tax ID; required for some tax filings and withholding calculations. |
| `invoice_number` | string | ✓ | "5654198635" | Unique identifier; critical for deduplication and audit lookup. |
| `invoice_date` | date (ISO) | ✗ | "2020-02-02" | Sets accrual date; must be ISO format for system compatibility. |
| `due_date` | date (ISO) | ✗ | "2020-02-07" | Payment deadline; used for early-pay discounts and aging reports. |
| `payment_terms` | string | ✓ | "Payable immediately" | Determines when cash is owed; affects cash flow forecasting. |
| `total_amount_due` | currency object | ✗ | `{ amount: 325512.64, iso_4217_currency_code: "PHP" }` | Bottom-line amount; MUST include currency code for multi-currency reconciliation. |
### Line items array
Each item captures:
- **description**: Service/product name — be verbose to enable code matching (e.g., "Containerimport { ExtendClient, extendCurrency, extendDate } from "extend-ai";
import { z } from "zod";
import fs from "fs";
const client = new ExtendClient({ token: process.env.EXTEND_API_KEY });
/**
* Vendor Invoice Processing Pipeline
*
* Parses and extracts structured data from vendor invoices including:
* - Vendor and customer information
* - Invoice metadata (date, number, payment terms)
* - Line items with multi-currency support
* - Tax details (vatable, zero-rated, exempt)
* - Shipment and container information
*/
const invoiceSchema = z.object({
// --- Vendor Information ---
vendor_name: z.string().nullable().describe(
"Full legal name of the vendor/service provider issuing the invoice, e.g. 'Sealand Maersk Asia Pte. Ltd.'"
),
vendor_address: z.string().nullable().describe(
"Complete physical address of the vendor, including street, city, postal code, country"
),
vendor_tax_id: z.string().nullable().describe(
"Tax identification number of the vendor, e.g. TIN, VAT ID, GST number. Format as shown on invoice"
),
// --- Customer Information ---
customer_name: z.string().nullable().describe(
"Full legal name of the customer/buyer receiving the invoice, e.g. 'BELLREX TRADING'"
),
customer_number: z.string().nullable().describe(
"Unique customer or account code assigned by the vendor, e.g. 'PH00269154'"
),
customer_address: z.string().nullable().describe(
"Complete physical address of the customer, including street, floor, building, city, postal code, country"
),
customer_tax_id: z.string().nullable().describe(
"Tax identification number of the customer/buyer, e.g. TIN, VAT ID, GST number"
),
// --- Invoice Metadata ---
invoice_number: z.string().nullable().describe(
"Unique invoice identifier assigned by the vendor, e.g. '5654198635'"
),
invoice_date: extendDate().describe(
"Date the invoice was issued, in ISO yyyy-mm-dd format, e.g. '2020-02-02'"
),
due_date: extendDate().describe(
"Payment due date in ISO yyyy-mm-dd format, e.g. '2020-02-07'"
),
payment_terms: z.string().nullable().describe(
"Payment terms or conditions, e.g. 'Net 30', 'Payable immediately', '2/10 Net 30'"
),
reference_number: z.string().nullable().describe(
"Optional reference or PO number for cross-referencing with purchase orders or internal systems"
),
// --- Line Items (Variable Length) ---
line_items: z.array(z.object({
description: z.string().nullable().describe(
"Service or product description, e.g. 'Container Cleaning Imports', 'Documentation fee - Destination'"
),
quantity: z.number().nullable().describe(
"Quantity or number of units, e.g. 7, 1"
),
unit_of_measure: z.string().nullable().describe(
"Unit of measurement, e.g. 'CNT' (containers), 'BOL' (bill of lading), 'EA' (each)"
),
unit_price: z.number().nullable().describe(
"Unit price before tax or surcharges, numeric value only"
),
currency: z.string().nullable().describe(
"ISO 4217 currency code, e.g. 'USD', 'PHP', 'EUR'"
),
exchange_rate: z.number().nullable().describe(
"Exchange rate applied if line item is in a different currency than invoice total, e.g. 51.456 (PHP per USD)"
),
total_amount: z.number().nullable().describe(
"Total for this line (quantity × unit_price, possibly adjusted by exchange rate), numeric value only"
),
})).describe(
"Array of itemized charges or services. Variable length; capture all line items present on the invoice."
),
// --- Tax Details (Variable Length) ---
tax_details: z.array(z.object({
tax_type: z.string().nullable().describe(
"Classification of the tax, e.g. 'Vatable Sales', 'Zero rated sales', 'VAT exempt sales'"
),
tax_rate_percent: z.number().nullable().describe(
"Tax rate as a percentage, e.g. 12 for 12%, 0 for 0%"
),
tax_base_amount: z.number().nullable().describe(
"Base amount on which the tax is calculated, numeric value only"
),
tax_amount: z.number().nullable().describe(
"Tax amount owed for this classification, numeric value only. May be 0 for exempt/zero-rated sales."
),
})).describe(
"Array of tax calculations by type. Capture all tax rows (vatable, zero-rated, exempt, etc.)."
),
// --- Total & Summary ---
total_amount_due: extendCurrency().describe(
"Total amount due after all charges and taxes, including currency code"
),
// --- Shipment Details ---
bill_of_lading_number: z.string().nullable().describe(
"Bill of lading (BOL) number for the shipment, e.g. '920667821'"
),
shipment_details: z.object({
etd: extendDate().describe("Estimated time of departure (ETD) in ISO yyyy-mm-dd format"),
eta: extendDate().describe("Estimated time of arrival (ETA) in ISO yyyy-mm-dd format"),
port_of_loading: z.string().nullable().describe("Origin port or place of loading, e.g. 'Xingang'"),
place_of_receipt: z.string().nullable().describe("Place where cargo is received, e.g. 'Xingang'"),
port_of_discharge: z.string().nullable().describe("Destination port, e.g. 'Davao City'"),
place_of_delivery: z.string().nullable().describe("Final delivery location, e.g. 'Davao City'"),
vessel_voyage: z.string().nullable().describe("Vessel name and voyage number, e.g. 'HAMMONIA BEROLINA/003S'"),
}).describe("Shipment routing and schedule information"),
// --- Container Details (Variable Length) ---
container_details: z.array(z.object({
container_number: z.string().nullable().describe(
"Container ID/number, e.g. 'HASU4458596', 'MRKU4692045'"
),
size_type: z.string().nullable().describe(
"Container size and type, e.g. '40/9'6/DRY' (40ft, 9'6\" high, dry container)"
),
service: z.string().nullable().describe(
"Service type, e.g. 'CY/CY' (container yard to container yard)"
),
pcd: extendDate().describe(
"Port closing date (PCD) or cut-off date in ISO yyyy-mm-dd format"
),
})).describe(
"Array of containers included in this shipment. Capture all containers listed."
),
});
export async function processVendorInvoice(filePath: string) {
console.log(`Processing vendor invoice: ${filePath}`);
// Convert local file to data URL for SDK compatibility
const fileBuffer = fs.readFileSync(filePath);
const dataUrl = `data:application/octet-stream;base64,${fileBuffer.toString("base64")}`;
// Step 1: Parse the invoice to markdown for readability and context
console.log("Step 1: Parsing invoice...");
const parseRun = await client.parseRuns.createAndPoll({
file: { url: dataUrl },
config: {
parseConfig: {
blockOptions: {
text: {
agentic: {
enabled: true, // Handles scans, logos, complex layouts
},
},
},
chunkingStrategy: {
type: "document", // Keep full invoice as one document
},
},
},
});
if (parseRun.status !== "PROCESSED") {
throw new Error(`Parse failed with status: ${parseRun.status}`);
}
const markdown = parseRun.output.chunks
.map((chunk) => chunk.content)
.join("\n\n");
console.log("✓ Invoice parsed to markdown\n");
// Step 2: Extract structured fields using Zod schema
console.log("Step 2: Extracting structured fields...");
const extractRun = await client.extractRuns.createAndPoll({
file: { url: dataUrl },
config: {
schema: invoiceSchema,
},
});
if (extractRun.status !== "PROCESSED") {
throw new Error(`Extraction failed with status: ${extractRun.status}`);
}
const extractedData = extractRun.output.value;
console.log("✓ Fields extracted\n");
// Return combined result
return {
status: "success",
invoice: {
markdown,
structured_data: extractedData,
},
};
}
// Auto-run if called directly
if (process.argv[2]) {
processVendorInvoice(process.argv[2])
.then((result) => {
console.log("\n=== EXTRACTION RESULT ===\n");
console.log(JSON.stringify(result.invoice.structured_data, null, 2));
})
.catch((err) => {
console.error("Pipeline error:", err.message);
process.exit(1);
});
}import os
import json
import base64
from typing import Optional
from extend_ai import Extend
# Initialize the Extend client
client = Extend(token=os.environ["EXTEND_API_KEY"])
# Define the extraction schema as a dictionary matching the provided JSON schema
invoice_schema = {
"type": "object",
"properties": {
"invoice_number": {
"type": ["string", "null"],
"description": "Unique invoice identifier"
},
"invoice_date": {
"type": ["string", "null"],
"description": "Date invoice was issued (DD.MMM.YYYY format)"
},
"due_date": {
"type": ["string", "null"],
"description": "Payment due date (DD.MMM.YYYY format)"
},
"bill_to_party": {
"type": ["string", "null"],
"description": "Customer name and address receiving the bill"
},
"customer_number": {
"type": ["string", "null"],
"description": "Customer reference number"
},
"bill_of_lading_number": {
"type": ["string", "null"],
"description": "Shipping bill of lading reference number"
},
"total_amount_due": {
"type": ["string", "null"],
"description": "Total invoice amount in PHP currency"
},
"vessel_voyage": {
"type": ["string", "null"],
"description": "Vessel name and voyage number"
},
"port_of_loading": {
"type": ["string", "null"],
"description": "Port where cargo originated"
},
"port_of_discharge": {
"type": ["string", "null"],
"description": "Final destination port"
},
"estimated_arrival": {
"type": ["string", "null"],
"description": "Estimated arrival date (DD-MMM-YYYY format)"
},
"vat_registration_number": {
"type": ["string", "null"],
"description": "VAT/Tax identification number of billing party"
}
}
}
def process_vendor_invoice(file_path: str) -> dict:
"""
Vendor Invoice Processing Pipeline
Parses and extracts structured data from vendor invoices including:
- Vendor and customer information
- Invoice metadata (date, number, payment terms)
- Line items with multi-currency support
- Tax details (vatable, zero-rated, exempt)
- Shipment and container information
"""
print(f"Processing vendor invoice: {file_path}")
# Convert local file to data URL for SDK compatibility
with open(file_path, "rb") as f:
file_buffer = f.read()
data_url = f"data:application/octet-stream;base64,{base64.b64encode(file_buffer).decode('utf-8')}"
# Step 1: Parse the invoice to markdown for readability and context
print("Step 1: Parsing invoice...")
parse_run = client.parse_runs.create_and_poll(
file={"url": data_url},
config={
"parse_config": {
"block_options": {
"text": {
"agentic": {
"enabled": True, # Handles scans, logos, complex layouts
},
},
},
"chunking_strategy": {
"type": "document", # Keep full invoice as one document
},
},
},
)
if parse_run.status != "PROCESSED":
raise Exception(f"Parse failed with status: {parse_run.status}")
markdown = "\n\n".join([chunk.content for chunk in parse_run.output.chunks])
print("✓ Invoice parsed to markdown\n")
# Step 2: Extract structured fields using schema
print("Step 2: Extracting structured fields...")
extract_run = client.extract_runs.create_and_poll(
file={"url": data_url},
config={
"schema": invoice_schema,
},
)
if extract_run.status != "PROCESSED":
raise Exception(f"Extraction failed with status: {extract_run.status}")
extracted_data = extract_run.output.value
print("✓ Fields extracted\n")
# Return combined result
return {
"status": "success",
"invoice": {
"markdown": markdown,
"structured_data": extracted_data,
},
}
# Auto-run if called with file path argument
if __name__ == "__main__":
import sys
if len(sys.argv) > 1:
file_path = sys.argv[1]
try:
result = process_vendor_invoice(file_path)
print("\n=== EXTRACTION RESULT ===\n")
print(json.dumps(result["invoice"]["structured_data"], indent=2))
except Exception as err:
print(f"Pipeline error: {str(err)}")
sys.exit(1)// Note: This code uses Extend's REST API directly because Extend has no official Java SDK.
// It calls https://api.extend.ai endpoints using only java.net.http.HttpClient (built-in, no external deps).
import java.io.*;
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 VendorInvoiceProcessor {
private static final String API_BASE = "https://api.extend.ai";
private final HttpClient httpClient;
private final String apiKey;
public VendorInvoiceProcessor(String apiKey) {
this.apiKey = apiKey;
this.httpClient = HttpClient.newHttpClient();
}
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("EXTEND_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("Error: EXTEND_API_KEY environment variable not set");
System.exit(1);
}
if (args.length < 1) {
System.err.println("Usage: java VendorInvoiceProcessor <file_path>");
System.exit(1);
}
VendorInvoiceProcessor processor = new VendorInvoiceProcessor(apiKey);
try {
Map<String, Object> result = processor.processVendorInvoice(args[0]);
System.out.println("\n=== EXTRACTION RESULT ===\n");
System.out.println(jsonStringify(result.get("invoice"), 2));
} catch (Exception e) {
System.err.println("Pipeline error: " + e.getMessage());
System.exit(1);
}
}
public Map<String, Object> processVendorInvoice(String filePath) throws Exception {
System.out.println("Processing vendor invoice: " + filePath);
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
String base64Data = Base64.getEncoder().encodeToString(fileBytes);
String dataUrl = "data:application/octet-stream;base64," + base64Data;
System.out.println("Step 1: Parsing invoice...");
Map<String, Object> parseResult = createAndPollParseRun(dataUrl);
if (!"PROCESSED".equals(parseResult.get("status"))) {
throw new Exception("Parse failed with status: " + parseResult.get("status"));
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> chunks =
(List<Map<String, Object>>) parseResult.get("chunks");
StringBuilder markdown = new StringBuilder();
for (Map<String, Object> chunk : chunks) {
if (markdown.length() > 0) markdown.append("\n\n");
markdown.append(chunk.get("content"));
}
System.out.println("✓ Invoice parsed to markdown\n");
System.out.println("Step 2: Extracting structured fields...");
Map<String, Object> extractResult = createAndPollExtractRun(dataUrl);
if (!"PROCESSED".equals(extractResult.get("status"))) {
throw new Exception("Extraction failed with status: " + extractResult.get("status"));
}
@SuppressWarnings("unchecked")
Map<String, Object> extractedData =
(Map<String, Object>) extractResult.get("value");
System.out.println("✓ Fields extracted\n");
Map<String, Object> invoice = new HashMap<>();
invoice.put("markdown", markdown.toString());
invoice.put("structured_data", extractedData);
Map<String, Object> result = new HashMap<>();
result.put("status", "success");
result.put("invoice", invoice);
return result;
}
private Map<String, Object> createAndPollParseRun(String dataUrl) throws Exception {
String parseConfigJson = "{"
+ "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
+ "\"config\":{"
+ "\"parseConfig\":{"
+ "\"blockOptions\":{\"text\":{\"agentic\":{\"enabled\":true}}},"
+ "\"chunkingStrategy\":{\"type\":\"document\"}"
+ "}"
+ "}"
+ "}";
String runId = createParseRun(parseConfigJson);
return pollParseRun(runId);
}
private String createParseRun(String bodyJson) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/v1/parse-runs"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(bodyJson))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new Exception("Parse run creation failed: " + response.body());
}
Map<String, Object> data = parseJson(response.body());
return (String) data.get("id");
}
private Map<String, Object> pollParseRun(String runId) throws Exception {
while (true) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/v1/parse-runs/" + runId))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new Exception("Parse run polling failed: " + response.body());
}
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) parseJson(response.body());
String status = (String) data.get("status");
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
return data;
}
Thread.sleep(2000);
}
}
private Map<String, Object> createAndPollExtractRun(String dataUrl) throws Exception {
String schema = buildInvoiceSchema();
String extractConfigJson = "{"
+ "\"file\":{\"url\":\"" + escapeJson(dataUrl) + "\"},"
+ "\"config\":{\"schema\":" + schema + "}"
+ "}";
String runId = createExtractRun(extractConfigJson);
return pollExtractRun(runId);
}
private String createExtractRun(String bodyJson) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/v1/extract-runs"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(bodyJson))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new Exception("Extract run creation failed: " + response.body());
}
Map<String, Object> data = parseJson(response.body());
return (String) data.get("id");
}
private Map<String, Object> pollExtractRun(String runId) throws Exception {
while (true) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/v1/extract-runs/" + runId))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new Exception("Extract run polling failed: " + response.body());
}
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) parseJson(response.body());
String status = (String) data.get("status");
if ("PROCESSED".equals(status) || "FAILED".equals(status)) {
return data;
}
Thread.sleep(2000);
}
}
private String buildInvoiceSchema() {
return "{\"type\":\"object\",\"properties\":{"
+ "\"invoice_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Unique invoice identifier\"},"
+ "\"invoice_date\":{\"type\":[\"string\",\"null\"],\"description\":\"Date invoice was issued (DD.MMM.YYYY format)\"},"
+ "\"due_date\":{\"type\":[\"string\",\"null\"],\"description\":\"Payment due date (DD.MMM.YYYY format)\"},"
+ "\"bill_to_party\":{\"type\":[\"string\",\"null\"],\"description\":\"Customer name and address receiving the bill\"},"
+ "\"customer_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Customer reference number\"},"
+ "\"bill_of_lading_number\":{\"type\":[\"string\",\"null\"],\"description\":\"Shipping bill of lading reference number\"},"
+ "\"total_amount_due\":{\"type\":[\"string\",\"null\"],\"description\":\"Total invoice amount in PHP currency\"},"
+ "\"vessel_voyage\":{\"type\":[\"string\",\"null\"],\"description\":\"Vessel name and voyage number\"},"
+ "\"port_of_loading\":{\"type\":[\"string\",\"null\"],\"description\":\"Port where cargo originated\"},"
+ "\"port_of_discharge\":{\"type\":[\"string\",\"null\"],\"description\":\"Final destination port\"},"
+ "\"estimated_arrival\":{\"type\":[\"string\",\"null\"],\"description\":\"Estimated arrival date (DD-MMM-YYYY format)\"},"
+ "\"vat_registration_number\":{\"type\":[\"string\",\"null\"],\"description\":\"VAT/Tax identification number of billing party\"}"
+ "}}";
}
private static Map<String, Object> parseJson(String json) throws Exception {
return new JsonParser().parse(json);
}
private static String jsonStringify(Object obj, int indent) {
return new JsonStringifier(indent).stringify(obj);
}
private static String escapeJson(String s) {
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
// Minimal JSON parser (no external dependencies)
static class JsonParser {
private String json;
private int pos;
public Map<String, Object> parse(String json) throws Exception {
this.json = json;
this.pos = 0;
skipWhitespace();
Object result = parseValue();
if (!(result instanceof Map)) {
throw new Exception("Expected JSON object at root");
}
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) result;
return map;
}
private Object parseValue() throws Exception {
skipWhitespace();
if (pos >= json.length()) throw new Exception("Unexpected end of JSON");
char ch = json.charAt(pos);
if (ch == '{') return parseObject();
if (ch == '[') return parseArray();
if (ch == '"') return parseString();
if (ch == 't' || ch == 'f') return parseBoolean();
if (ch == 'n') return parseNull();
return parseNumber();
}
private Map<String, Object> parseObject() throws Exception {
Map<String, Object> obj = new LinkedHashMap<>();
pos++; // skip '{'
skipWhitespace();
if (pos < json.length() && json.charAt(pos) == '}') {
pos++;
return obj;
}
while (true) {
skipWhitespace();
String key = parseString();
skipWhitespace();
if (pos >= json.length() || json.charAt(pos) != ':')
throw new Exception("Expected ':' in object");
pos++;
Object value = parseValue();
obj.put(key, value);
skipWhitespace();
if (pos >= json.length()) throw new Exception("Unexpected end of JSON");
char ch = json.charAt(pos);
if (ch == '}') {
pos++;
break;
}
if (ch != ',') throw new Exception("Expected ',' or '}' in object");
pos++;
}
return obj;
}
private List<Object> parseArray() throws Exception {
List<Object> arr = new ArrayList<>();
pos++; // skip '['
skipWhitespace();
if (pos < json.length() && json.charAt(pos) == ']') {
pos++;
return arr;
}
while (true) {
arr.add(parseValue());
skipWhitespace();
if (pos >= json.length()) throw new Exception("Unexpected end of JSON");
char ch = json.charAt(pos);
if (ch == ']') {
pos++;
break;
}
if (ch != ',') throw new Exception("Expected ',' or ']' in array");
pos++;
}
return arr;
}
private String parseString() throws Exception {
if (pos >= json.length() || json.charAt(pos) != '"')
throw new Exception("Expected '\"'");
pos++;
StringBuilder sb = new StringBuilder();
while (pos < json.length()) {
char ch = json.charAt(pos);
if (ch == '"') {
pos++;
return sb.toString();
}
if (ch == '\\') {
pos++;
if (pos >= json.length()) throw new Exception("Unexpected end in string escape");
char esc = json.charAt(pos);
if (esc == '"' || esc == '\\' || esc == '/') sb.append(esc);
else if (esc == 'b') sb.append('\b');
else if (esc == 'f') sb.append('\f');
else if (esc == 'n') sb.append('\n');
else if (esc == 'r') sb.append('\r');
else if (esc == 't') sb.append('\t');
else if (esc == 'u') {
pos++;
int code = Integer.parseInt(json.substring(pos, pos + 4), 16);
sb.append((char) code);
pos += 3;
} else throw new Exception("Unknown escape sequence");
pos++;
} else {
sb.append(ch);
pos++;
}
}
throw new Exception("Unterminated string");
}
private Number parseNumber() throws Exception {
int start = pos;
if (pos < json.length() && json.charAt(pos) == '-') pos++;
while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++;
if (pos < json.length() && json.charAt(pos) == '.') {
pos++;
while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++;
}
if (pos < json.length() && (json.charAt(pos) == 'e' || json.charAt(pos) == 'E')) {
pos++;
if (pos < json.length() && (json.charAt(pos) == '+' || json.charAt(pos) == '-')) pos++;
while (pos < json.length() && Character.isDigit(json.charAt(pos))) pos++;
}
String numStr = json.substring(start, pos);
if (numStr.contains(".") || numStr.contains("e") || numStr.contains("E")) {
return Double.parseDouble(numStr);
}
return Long.parseLong(numStr);
}
private Boolean parseBoolean() throws Exception {
if (json.startsWith("true", pos)) {
pos += 4;
return true;
}
if (json.startsWith("false", pos)) {
pos += 5;
return false;
}
throw new Exception("Invalid boolean");
}
private Object parseNull() throws Exception {
if (json.startsWith("null", pos)) {
pos += 4;
return null;
}
throw new Exception("Invalid null");
}
private void skipWhitespace() {
while (pos < json.length() && Character.isWhitespace(json.charAt(pos))) pos++;
}
}
// Minimal JSON stringifier
static class JsonStringifier {
private int indent;
private int depth;
JsonStringifier(int indent) {
this.indent = indent;
this.depth = 0;
}
String stringify(Object obj) {
return stringifyValue(obj);
}
private String stringifyValue(Object obj) {
if (obj == null) return "null";
if (obj instanceof Boolean) return obj.toString();
if (obj instanceof Number) return obj.toString();
if (obj instanceof String) return "\"" + escapeJson((String) obj) + "\"";
if (obj instanceof Map) return stringifyObject((Map<?, ?>) obj);
if (obj instanceof List) return stringifyArray((List<?>) obj);
return "null";
}
private String stringifyObject(Map<?, ?> map) {
StringBuilder sb = new StringBuilder("{");
depth++;
boolean first = true;
for (Map.Entry<?, ?> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\n").append(spaces(depth * indent))
.append("\"").append(escapeJson(entry.getKey().toString())).append("\": ")
.append(stringifyValue(entry.getValue()));
first = false;
}
depth--;
if (!first) sb.append("\n").append(spaces(depth * indent));
sb.append("}");
return sb.toString();
}
private String stringifyArray(List<?> list) {
StringBuilder sb = new StringBuilder("[");
depth++;
boolean first = true;
for (Object item : list) {
if (!first) sb.append(",");
sb.append("\n").append(spaces(depth * indent)).append(stringifyValue(item));
first = false;
}
depth--;
if (!first) sb.append("\n").append(spaces(depth * indent));
sb.append("]");
return sb.toString();
}
private String spaces(int count) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) sb.append(" ");
return sb.toString();
}
}
}// This code calls the Extend REST API directly using only Go's standard library.
// Extend does not publish an official Go SDK; this uses net/http and encoding/json
// to match the exact same endpoints and JSON shapes that the TypeScript SDK wraps.
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const extendAPIBaseURL = "https://api.extend.ai"
// InvoiceSchema represents the vendor invoice extraction schema
type InvoiceSchema struct {
VendorName *string `json:"vendor_name"`
VendorAddress *string `json:"vendor_address"`
VendorTaxID *string `json:"vendor_tax_id"`
CustomerName *string `json:"customer_name"`
CustomerNumber *string `json:"customer_number"`
CustomerAddress *string `json:"customer_address"`
CustomerTaxID *string `json:"customer_tax_id"`
InvoiceNumber *string `json:"invoice_number"`
InvoiceDate *string `json:"invoice_date"`
DueDate *string `json:"due_date"`
PaymentTerms *string `json:"payment_terms"`
ReferenceNumber *string `json:"reference_number"`
LineItems []LineItem `json:"line_items"`
TaxDetails []TaxDetail `json:"tax_details"`
TotalAmountDue *Currency `json:"total_amount_due"`
BillOfLadingNumber *string `json:"bill_of_lading_number"`
ShipmentDetails ShipmentDetail `json:"shipment_details"`
ContainerDetails []ContainerDtl `json:"container_details"`
}
type LineItem struct {
Description *string `json:"description"`
Quantity *float64 `json:"quantity"`
UnitOfMeasure *string `json:"unit_of_measure"`
UnitPrice *float64 `json:"unit_price"`
Currency *string `json:"currency"`
ExchangeRate *float64 `json:"exchange_rate"`
TotalAmount *float64 `json:"total_amount"`
}
type TaxDetail struct {
TaxType *string `json:"tax_type"`
TaxRatePercent *float64 `json:"tax_rate_percent"`
TaxBaseAmount *float64 `json:"tax_base_amount"`
TaxAmount *float64 `json:"tax_amount"`
}
type Currency struct {
Amount *float64 `json:"amount"`
Currency *string `json:"currency"`
}
type ShipmentDetail struct {
ETD *string `json:"etd"`
ETA *string `json:"eta"`
PortOfLoading *string `json:"port_of_loading"`
PlaceOfReceipt *string `json:"place_of_receipt"`
PortOfDischarge *string `json:"port_of_discharge"`
PlaceOfDelivery *string `json:"place_of_delivery"`
VesselVoyage *string `json:"vessel_voyage"`
}
type ContainerDtl struct {
ContainerNumber *string `json:"container_number"`
SizeType *string `json:"size_type"`
Service *string `json:"service"`
PCD *string `json:"pcd"`
}
// ParseRunOutput represents the output of a parse run
type ParseRunOutput struct {
Chunks []struct {
Content string `json:"content"`
} `json:"chunks"`
}
// ParseRun represents a parse operation result
type ParseRun struct {
Status string `json:"status"`
Output ParseRunOutput `json:"output"`
}
// ExtractRun represents an extract operation result
type ExtractRun struct {
Status string `json:"status"`
Output ExtractOutput `json:"output"`
}
type ExtractOutput struct {
Value *InvoiceSchema `json:"value"`
}
// ProcessVendorInvoice processes a vendor invoice file
func ProcessVendorInvoice(filePath string, apiKey string) (map[string]interface{}, error) {
fmt.Printf("Processing vendor invoice: %s\n", filePath)
// Read file and convert to base64 data URL
fileBuffer, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}
dataURL := fmt.Sprintf("data:application/octet-stream;base64,%s", base64.StdEncoding.EncodeToString(fileBuffer))
// Step 1: Parse the invoice to markdown
fmt.Println("Step 1: Parsing invoice...")
markdown, err := parseInvoice(dataURL, apiKey)
if err != nil {
return nil, fmt.Errorf("parse failed: %w", err)
}
fmt.Println("✓ Invoice parsed to markdown\n")
// Step 2: Extract structured fields
fmt.Println("Step 2: Extracting structured fields...")
extractedData, err := extractInvoiceFields(dataURL, apiKey)
if err != nil {
return nil, fmt.Errorf("extraction failed: %w", err)
}
fmt.Println("✓ Fields extracted\n")
// Return combined result
return map[string]interface{}{
"status": "success",
"invoice": map[string]interface{}{
"markdown": markdown,
"structured_data": extractedData,
},
}, nil
}
func parseInvoice(dataURL, apiKey string) (string, error) {
payloadMap := map[string]interface{}{
"file": map[string]interface{}{
"url": dataURL,
},
"config": map[string]interface{}{
"parseConfig": map[string]interface{}{
"blockOptions": map[string]interface{}{
"text": map[string]interface{}{
"agentic": map[string]interface{}{
"enabled": true,
},
},
},
"chunkingStrategy": map[string]interface{}{
"type": "document",
},
},
},
}
payload, err := json.Marshal(payloadMap)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", extendAPIBaseURL+"/parseRuns/create", bytes.NewBuffer(payload))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var parseRun ParseRun
if err := json.Unmarshal(body, &parseRun); err != nil {
return "", err
}
if parseRun.Status != "PROCESSED" {
return "", fmt.Errorf("parse failed with status: %s", parseRun.Status)
}
var markdown string
for _, chunk := range parseRun.Output.Chunks {
markdown += chunk.Content + "\n\n"
}
return markdown, nil
}
func extractInvoiceFields(dataURL, apiKey string) (*InvoiceSchema, error) {
schema := map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"vendor_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Full legal name of the vendor/service provider issuing the invoice",
},
"vendor_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Complete physical address of the vendor",
},
"vendor_tax_id": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Tax identification number of the vendor",
},
"customer_name": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Full legal name of the customer/buyer",
},
"customer_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Unique customer or account code",
},
"customer_address": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Complete physical address of the customer",
},
"customer_tax_id": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Tax identification number of the customer/buyer",
},
"invoice_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Unique invoice identifier",
},
"invoice_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Date invoice was issued in ISO yyyy-mm-dd format",
},
"due_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Payment due date in ISO yyyy-mm-dd format",
},
"payment_terms": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Payment terms or conditions",
},
"reference_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Optional reference or PO number",
},
"line_items": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"description": map[string]interface{}{"type": []string{"string", "null"}},
"quantity": map[string]interface{}{"type": []string{"number", "null"}},
"unit_of_measure": map[string]interface{}{"type": []string{"string", "null"}},
"unit_price": map[string]interface{}{"type": []string{"number", "null"}},
"currency": map[string]interface{}{"type": []string{"string", "null"}},
"exchange_rate": map[string]interface{}{"type": []string{"number", "null"}},
"total_amount": map[string]interface{}{"type": []string{"number", "null"}},
},
},
},
"tax_details": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"tax_type": map[string]interface{}{"type": []string{"string", "null"}},
"tax_rate_percent": map[string]interface{}{"type": []string{"number", "null"}},
"tax_base_amount": map[string]interface{}{"type": []string{"number", "null"}},
"tax_amount": map[string]interface{}{"type": []string{"number", "null"}},
},
},
},
"total_amount_due": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"amount": map[string]interface{}{"type": []string{"number", "null"}},
"currency": map[string]interface{}{"type": []string{"string", "null"}},
},
},
"bill_of_lading_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Bill of lading number for the shipment",
},
"shipment_details": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"etd": map[string]interface{}{"type": []string{"string", "null"}},
"eta": map[string]interface{}{"type": []string{"string", "null"}},
"port_of_loading": map[string]interface{}{"type": []string{"string", "null"}},
"place_of_receipt": map[string]interface{}{"type": []string{"string", "null"}},
"port_of_discharge": map[string]interface{}{"type": []string{"string", "null"}},
"place_of_delivery": map[string]interface{}{"type": []string{"string", "null"}},
"vessel_voyage": map[string]interface{}{"type": []string{"string", "null"}},
},
},
"container_details": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"container_number": map[string]interface{}{"type": []string{"string", "null"}},
"size_type": map[string]interface{}{"type": []string{"string", "null"}},
"service": map[string]interface{}{"type": []string{"string", "null"}},
"pcd": map[string]interface{}{"type": []string{"string", "null"}},
},
},
},
},
}
payloadMap := map[string]interface{}{
"file": map[string]interface{}{
"url": dataURL,
},
"config": map[string]interface{}{
"schema": schema,
},
}
payload, err := json.Marshal(payloadMap)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", extendAPIBaseURL+"/extractRuns/create", bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var extractRun ExtractRun
if err := json.Unmarshal(body, &extractRun); err != nil {
return nil, err
}
if extractRun.Status != "PROCESSED" {
return nil, fmt.Errorf("extraction failed with status: %s", extractRun.Status)
}
return extractRun.Output.Value, nil
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run solution.go <invoice_file_path>")
os.Exit(1)
}
apiKey := os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Println("Error: EXTEND_API_KEY environment variable not set")
os.Exit(1)
}
result, err := ProcessVendorInvoice(os.Args[1], apiKey)
if err != nil {
fmt.Printf("Pipeline error: %v\n", err)
os.Exit(1)
}
fmt.Println("\n=== EXTRACTION RESULT ===\n")
jsonOut, err := json.MarshalIndent(result["invoice"].(map[string]interface{})["structured_data"], "", " ")
if err != nil {
fmt.Printf("Error marshaling result: %v\n", err)
os.Exit(1)
}
fmt.Println(string(jsonOut))
}// Deploy the "Vendor Invoice" 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/vendor-invoice.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: vendor-invoice).
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, "vendor-invoice.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": "Vendor Invoice Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [
{
"step": "parse1"
}
]
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {
"agentic": {
"enabled": true
}
}
},
"chunkingStrategy": {
"type": "document"
}
}
},
"next": [
{
"step": "extraction2"
}
]
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"invoice_number": {
"type": [
"string",
"null"
],
"description": "Unique invoice identifier"
},
"invoice_date": {
"type": [
"string",
"null"
],
"description": "Date invoice was issued (DD.MMM.YYYY format)"
},
"due_date": {
"type": [
"string",
"null"
],
"description": "Payment due date (DD.MMM.YYYY format)"
},
"bill_to_party": {
"type": [
"string",
"null"
],
"description": "Customer name and address receiving the bill"
},
"customer_number": {
"type": [
"string",
"null"
],
"description": "Customer reference number"
},
"bill_of_lading_number": {
"type": [
"string",
"null"
],
"description": "Shipping bill of lading reference number"
},
"total_amount_due": {
"type": [
"string",
"null"
],
"description": "Total invoice amount in PHP currency"
},
"vessel_voyage": {
"type": [
"string",
"null"
],
"description": "Vessel name and voyage number"
},
"port_of_loading": {
"type": [
"string",
"null"
],
"description": "Port where cargo originated"
},
"port_of_discharge": {
"type": [
"string",
"null"
],
"description": "Final destination port"
},
"estimated_arrival": {
"type": [
"string",
"null"
],
"description": "Estimated arrival date (DD-MMM-YYYY format)"
},
"vat_registration_number": {
"type": [
"string",
"null"
],
"description": "VAT/Tax identification number of billing party"
}
}
}
}
}
}
]
};
async function main() {
console.log(`Deploying "${WORKFLOW.name}"…`);
if (state.workflowId) {
console.log(`✓ workflow already provisioned (${state.workflowId}) — updating steps`);
await api("POST", `/workflows/${state.workflowId}`, { steps: WORKFLOW.steps });
} else {
// Reuse an existing workflow with the same name if one exists (e.g. a
// previous run's state file was lost) instead of creating a duplicate.
try {
const list = await api("GET", `/workflows?name=${encodeURIComponent(WORKFLOW.name)}`);
const items = (list.data ?? list.items ?? []) as Array<{ name?: string; id?: string }>;
const existing = items.find((x) => x.name === WORKFLOW.name);
if (existing?.id) {
state.workflowId = existing.id; saveState();
console.log(`✓ workflow "${WORKFLOW.name}" found in your account (${existing.id}) — updating steps`);
await api("POST", `/workflows/${existing.id}`, { steps: WORKFLOW.steps });
}
} catch { /* lookup is best-effort; fall through to create */ }
if (!state.workflowId) {
const created = await api("POST", "/workflows", WORKFLOW);
const wfId = created.id ?? created.workflow?.id;
if (!wfId) throw new Error("Could not read created workflow id from response");
state.workflowId = wfId; saveState();
console.log(`+ created workflow (${wfId})`);
}
}
// Deploy the current draft as a new version so the workflow is runnable —
// best-effort: some accounts/plans may not require this explicit step.
await api("POST", `/workflows/${state.workflowId}/versions`, {}).catch(() => {});
console.log("\nDone. Run documents through it with:");
console.log(` POST ${API}/workflow_runs { workflow: { id: "${state.workflowId}" }, file: { url: "https://…" } }`);
console.log("Or open the workflow in the Extend dashboard to review and deploy it.");
}
main().catch((e) => { console.error(e.message ?? e); process.exit(1); });
import os
import json
import sys
from pathlib import Path
from typing import Any, Optional
import urllib.parse
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 / "vendor-invoice.json"
state: dict[str, Optional[str]] = {}
if STATE_FILE.exists():
with open(STATE_FILE, "r") as f:
state = json.load(f)
def save_state() -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=2)
WORKFLOW = {
"name": "Vendor Invoice Processing Pipeline",
"steps": [
{
"name": "startTrigger1",
"type": "TRIGGER",
"next": [{"step": "parse1"}],
},
{
"name": "parse1",
"type": "PARSE",
"config": {
"parseConfig": {
"blockOptions": {
"text": {"agentic": {"enabled": True}}
},
"chunkingStrategy": {"type": "document"},
}
},
"next": [{"step": "extraction2"}],
},
{
"name": "extraction2",
"type": "EXTRACT",
"config": {
"extractorConfig": {
"schema": {
"type": "object",
"properties": {
"invoice_number": {
"type": ["string", "null"],
"description": "Unique invoice identifier",
},
"invoice_date": {
"type": ["string", "null"],
"description": "Date invoice was issued (DD.MMM.YYYY format)",
},
"due_date": {
"type": ["string", "null"],
"description": "Payment due date (DD.MMM.YYYY format)",
},
"bill_to_party": {
"type": ["string", "null"],
"description": "Customer name and address receiving the bill",
},
"customer_number": {
"type": ["string", "null"],
"description": "Customer reference number",
},
"bill_of_lading_number": {
"type": ["string", "null"],
"description": "Shipping bill of lading reference number",
},
"total_amount_due": {
"type": ["string", "null"],
"description": "Total invoice amount in PHP currency",
},
"vessel_voyage": {
"type": ["string", "null"],
"description": "Vessel name and voyage number",
},
"port_of_loading": {
"type": ["string", "null"],
"description": "Port where cargo originated",
},
"port_of_discharge": {
"type": ["string", "null"],
"description": "Final destination port",
},
"estimated_arrival": {
"type": ["string", "null"],
"description": "Estimated arrival date (DD-MMM-YYYY format)",
},
"vat_registration_number": {
"type": ["string", "null"],
"description": "VAT/Tax identification number of billing party",
},
},
}
}
},
},
],
}
def main() -> None:
client = Extend(token=API_KEY)
print(f'Deploying "{WORKFLOW["name"]}…"')
workflow_id = state.get("workflowId")
if workflow_id:
print(f"✓ workflow already provisioned ({workflow_id}) — updating steps")
client.workflows.update(
workflow_id, steps=WORKFLOW["steps"]
)
else:
# Reuse an existing workflow with the same name if one exists
existing_id: Optional[str] = None
try:
workflows_list = client.workflows.list(
name=WORKFLOW["name"]
)
items = workflows_list.data or workflows_list.items or []
for item in items:
if item.name == WORKFLOW["name"]:
existing_id = item.id
break
except Exception:
# Lookup is best-effort; fall through to create
pass
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(
existing_id, steps=WORKFLOW["steps"]
)
else:
created = client.workflows.create(**WORKFLOW)
workflow_id = created.id
if not workflow_id:
raise RuntimeError("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 —
# best-effort: some accounts/plans may not require this explicit step.
try:
client.workflows.create_version(state["workflowId"])
except Exception:
pass
print("\nDone. Run documents through it with:")
print(
f' POST https://api.extend.ai/workflow_runs {{ "workflow": {{ "id": "{state["workflowId"]}" }}, "file": {{ "url": "https://…" }} }}'
)
print(
"Or open the workflow in the Extend dashboard to review and deploy it."
)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(str(e), file=sys.stderr)
sys.exit(1)// Extend REST API client — Extend has no official Java SDK yet, so we call the API directly.
// Deploy the "Vendor Invoice" 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/vendor-invoice.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)
// javac Provision.java && java Provision
//
// Generated by doc1 (template: vendor-invoice).
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.Map;
import java.util.List;
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 final Path STATE_DIR = Paths.get(System.getProperty("user.dir"), ".extend");
private static final Path STATE_FILE = STATE_DIR.resolve("vendor-invoice.json");
static {
if (API_KEY == null || API_KEY.isEmpty()) {
System.err.println("Set EXTEND_API_KEY first.");
System.exit(1);
}
}
private static class State {
String workflowId;
static State load() throws IOException {
State state = new State();
if (Files.exists(STATE_FILE)) {
String json = Files.readString(STATE_FILE);
Map<String, Object> map = parseJson(json);
if (map.containsKey("workflowId")) {
state.workflowId = (String) map.get("workflowId");
}
}
return state;
}
void save() throws IOException {
Files.createDirectories(STATE_DIR);
StringBuilder sb = new StringBuilder("{\n \"workflowId\": ");
if (workflowId != null) {
sb.append("\"").append(workflowId).append("\"");
} else {
sb.append("null");
}
sb.append("\n}");
Files.writeString(STATE_FILE, sb.toString());
}
}
private static Map<String, Object> parseJson(String json) {
return new SimpleJsonParser().parse(json);
}
private static String toJson(Object obj) {
return new SimpleJsonBuilder().build(obj);
}
private static class SimpleJsonParser {
@SuppressWarnings("unchecked")
Map<String, Object> parse(String json) {
json = json.trim();
if (!json.startsWith("{")) return Map.of();
json = json.substring(1, json.length() - 1).trim();
Map<String, Object> result = new java.util.HashMap<>();
if (json.isEmpty()) return result;
int depth = 0;
int start = 0;
for (int i = 0; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '{' || c == '[') depth++;
else if (c == '}' || c == ']') depth--;
else if (c == ':' && depth == 0) {
String key = json.substring(start, i).trim();
if (key.startsWith("\"") && key.endsWith("\"")) {
key = key.substring(1, key.length() - 1);
}
int valEnd = i + 1;
depth = 0;
while (valEnd < json.length()) {
char vc = json.charAt(valEnd);
if (vc == '{' || vc == '[') depth++;
else if (vc == '}' || vc == ']') depth--;
else if (vc == ',' && depth == 0) break;
valEnd++;
}
String valStr = json.substring(i + 1, valEnd).trim();
Object val = null;
if (valStr.startsWith("\"") && valStr.endsWith("\"")) {
val = valStr.substring(1, valStr.length() - 1);
} else if (valStr.equals("null")) {
val = null;
} else if (valStr.equals("true")) {
val = true;
} else if (valStr.equals("false")) {
val = false;
}
result.put(key, val);
start = valEnd + 1;
i = valEnd;
}
}
return result;
}
}
private static class SimpleJsonBuilder {
String build(Object obj) {
if (obj == null) return "null";
if (obj instanceof String) return "\"" + escape((String) obj) + "\"";
if (obj instanceof Boolean) return obj.toString();
if (obj instanceof Number) return obj.toString();
if (obj instanceof Map) return buildMap((Map<?, ?>) obj);
if (obj instanceof List) return buildList((List<?>) obj);
return "null";
}
private String buildMap(Map<?, ?> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<?, ?> e : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(escape(e.getKey().toString())).append("\":");
sb.append(build(e.getValue()));
first = false;
}
sb.append("}");
return sb.toString();
}
private String buildList(List<?> list) {
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (Object item : list) {
if (!first) sb.append(",");
sb.append(build(item));
first = false;
}
sb.append("]");
return sb.toString();
}
private String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
private static Object apiCall(String method, String pathName, Object body) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String url = API + pathName;
HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
.uri(new URI(url))
.header("Authorization", "Bearer " + API_KEY)
.header("x-extend-api-version", VERSION);
if (body != null) {
String jsonBody = toJson(body);
reqBuilder.header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
reqBuilder.method(method, HttpRequest.BodyPublishers.noBody());
}
HttpRequest req = reqBuilder.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
String responseBody = res.body();
Object data = null;
try {
if (!responseBody.isEmpty()) {
data = parseJson(responseBody);
}
} catch (Exception e) {
data = Map.of();
}
if (res.statusCode() < 200 || res.statusCode() >= 300) {
String errorMsg = toJson(data);
if (errorMsg.length() > 300) errorMsg = errorMsg.substring(0, 300);
throw new Exception(method + " " + pathName + " failed (" + res.statusCode() + "): " + errorMsg);
}
return data;
}
private static Map<String, Object> buildWorkflow() {
Map<String, Object> workflow = new java.util.LinkedHashMap<>();
workflow.put("name", "Vendor Invoice Processing Pipeline");
List<Map<String, Object>> steps = new java.util.ArrayList<>();
// startTrigger1
Map<String, Object> startTrigger = new java.util.LinkedHashMap<>();
startTrigger.put("name", "startTrigger1");
startTrigger.put("type", "TRIGGER");
Map<String, Object> nextStep1 = new java.util.LinkedHashMap<>();
nextStep1.put("step", "parse1");
startTrigger.put("next", List.of(nextStep1));
steps.add(startTrigger);
// parse1
Map<String, Object> parse1 = new java.util.LinkedHashMap<>();
parse1.put("name", "parse1");
parse1.put("type", "PARSE");
Map<String, Object> parseConfig = new java.util.LinkedHashMap<>();
Map<String, Object> blockOptions = new java.util.LinkedHashMap<>();
Map<String, Object> textOpt = new java.util.LinkedHashMap<>();
Map<String, Object> agentic = new java.util.LinkedHashMap<>();
agentic.put("enabled", true);
textOpt.put("agentic", agentic);
blockOptions.put("text", textOpt);
parseConfig.put("blockOptions", blockOptions);
Map<String, Object> chunkingStrategy = new java.util.LinkedHashMap<>();
chunkingStrategy.put("type", "document");
parseConfig.put("chunkingStrategy", chunkingStrategy);
Map<String, Object> config1 = new java.util.LinkedHashMap<>();
config1.put("parseConfig", parseConfig);
parse1.put("config", config1);
Map<String, Object> nextStep2 = new java.util.LinkedHashMap<>();
nextStep2.put("step", "extraction2");
parse1.put("next", List.of(nextStep2));
steps.add(parse1);
// extraction2
Map<String, Object> extraction2 = new java.util.LinkedHashMap<>();
extraction2.put("name", "extraction2");
extraction2.put("type", "EXTRACT");
Map<String, Object> extractorConfig = new java.util.LinkedHashMap<>();
Map<String, Object> schema = new java.util.LinkedHashMap<>();
schema.put("type", "object");
Map<String, Object> properties = new java.util.LinkedHashMap<>();
String[][] fields = {
{"invoice_number", "Unique invoice identifier"},
{"invoice_date", "Date invoice was issued (DD.MMM.YYYY format)"},
{"due_date", "Payment due date (DD.MMM.YYYY format)"},
{"bill_to_party", "Customer name and address receiving the bill"},
{"customer_number", "Customer reference number"},
{"bill_of_lading_number", "Shipping bill of lading reference number"},
{"total_amount_due", "Total invoice amount in PHP currency"},
{"vessel_voyage", "Vessel name and voyage number"},
{"port_of_loading", "Port where cargo originated"},
{"port_of_discharge", "Final destination port"},
{"estimated_arrival", "Estimated arrival date (DD-MMM-YYYY format)"},
{"vat_registration_number", "VAT/Tax identification number of billing party"}
};
for (String[] field : fields) {
Map<String, Object> prop = new java.util.LinkedHashMap<>();
prop.put("type", List.of("string", "null"));
prop.put("description", field[1]);
properties.put(field[0], prop);
}
schema.put("properties", properties);
extractorConfig.put("schema", schema);
Map<String, Object> config2 = new java.util.LinkedHashMap<>();
config2.put("extractorConfig", extractorConfig);
extraction2.put("config", config2);
steps.add(extraction2);
workflow.put("steps", steps);
return workflow;
}
public static void main(String[] args) throws Exception {
Map<String, Object> workflow = buildWorkflow();
String workflowName = (String) workflow.get("name");
List<?> workflowSteps = (List<?>) workflow.get("steps");
System.out.println("Deploying \"" + workflowName + "\"…");
State state = State.load();
if (state.workflowId != null && !state.workflowId.isEmpty()) {
System.out.println("✓ workflow already provisioned (" + state.workflowId + ") — updating steps");
Map<String, Object> updateBody = new java.util.LinkedHashMap<>();
updateBody.put("steps", workflowSteps);
apiCall("POST", "/workflows/" + state.workflowId, updateBody);
} else {
boolean found = false;
try {
String queryUrl = "/workflows?name=" + java.net.URLEncoder.encode(workflowName, "UTF-8");
Object listResp = apiCall("GET", queryUrl, null);
if (listResp instanceof Map) {
Map<String, Object> respMap = (Map<String, Object>) listResp;
List<?> items = null;
if (respMap.containsKey("data")) {
items = (List<?>) respMap.get("data");
} else if (respMap.containsKey("items")) {
items = (List<?>) respMap.get("items");
} else {
items = List.of();
}
for (Object item : items) {
if (item instanceof Map) {
Map<String, Object> itemMap = (Map<String, Object>) item;
if (workflowName.equals(itemMap.get("name"))) {
String existingId = (String) itemMap.get("id");
if (existingId != null) {
state.workflowId = existingId;
state.save();
System.out.println("✓ workflow \"" + workflowName + "\" found in your account (" + existingId + ") — updating steps");
Map<String, Object> updateBody = new java.util.LinkedHashMap<>();
updateBody.put("steps", workflowSteps);
apiCall("POST", "/workflows/" + existingId, updateBody);
found = true;
break;
}
}
}
}
}
} catch (Exception e) {
// lookup is best-effort; fall through to create
}
if (!found && (state.workflowId == null || state.workflowId.isEmpty())) {
Object created = apiCall("POST", "/workflows", workflow);
String wfId = null;
if (created instanceof Map) {
Map<String, Object> createdMap = (Map<String, Object>) created;
if (createdMap.containsKey("id")) {
wfId = (String) createdMap.get("id");
} else if (createdMap.containsKey("workflow")) {
Map<String, Object> wf = (Map<String, Object>) createdMap.get("workflow");
wfId = (String) wf.get("id");
}
}
if (wfId == null) {
throw new Exception("Could not read created workflow id from response");
}
state.workflowId = wfId;
state.save();
System.out.println("+ 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.
try {
apiCall("POST", "/workflows/" + state.workflowId + "/versions", new java.util.LinkedHashMap<>());
} catch (Exception e) {
// best-effort
}
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.");
}
}// This script uses the Extend REST API directly because Extend has no official Go SDK yet.
// Calls are made using only Go's standard net/http and encoding/json packages.
//
// Deploy the "Vendor Invoice" 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/vendor-invoice.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)
// go run provision.go
//
// Generated by doc1 (template: vendor-invoice).
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
)
const (
API = "https://api.extend.ai"
VERSION = "2026-02-09"
)
var (
apiKey string
stateDir string
stateFile string
)
type State struct {
WorkflowID string `json:"workflowId,omitempty"`
}
type WorkflowStep struct {
Name string `json:"name"`
Type string `json:"type"`
Config interface{} `json:"config,omitempty"`
Next []interface{} `json:"next,omitempty"`
}
type Workflow struct {
Name string `json:"name"`
Steps []WorkflowStep `json:"steps"`
}
type APIResponse struct {
ID string `json:"id,omitempty"`
Workflow *struct {
ID string `json:"id,omitempty"`
} `json:"workflow,omitempty"`
Data []interface{} `json:"data,omitempty"`
Items []interface{} `json:"items,omitempty"`
}
func init() {
apiKey = os.Getenv("EXTEND_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Set EXTEND_API_KEY first.\n")
os.Exit(1)
}
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
os.Exit(1)
}
stateDir = filepath.Join(wd, ".extend")
stateFile = filepath.Join(stateDir, "vendor-invoice.json")
}
func loadState() State {
data, err := os.ReadFile(stateFile)
if err != nil {
return State{}
}
var s State
json.Unmarshal(data, &s)
return s
}
func saveState(s State) error {
if err := os.MkdirAll(stateDir, 0755); err != nil {
return err
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(stateFile, data, 0644)
}
func apiCall(method, pathName string, body interface{}) (APIResponse, error) {
var reqBody io.Reader
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
return APIResponse{}, err
}
reqBody = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequest(method, API+pathName, reqBody)
if err != nil {
return APIResponse{}, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("x-extend-api-version", VERSION)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return APIResponse{}, err
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return APIResponse{}, err
}
var data APIResponse
json.Unmarshal(respBytes, &data)
if !resp.Ok {
if len(respBytes) > 300 {
respBytes = respBytes[:300]
}
return APIResponse{}, fmt.Errorf("%s %s failed (%d): %s", method, pathName, resp.StatusCode, string(respBytes))
}
return data, nil
}
func main() {
workflow := Workflow{
Name: "Vendor Invoice 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,
},
},
},
"chunkingStrategy": map[string]string{
"type": "document",
},
},
},
Next: []interface{}{
map[string]string{"step": "extraction2"},
},
},
{
Name: "extraction2",
Type: "EXTRACT",
Config: map[string]interface{}{
"extractorConfig": map[string]interface{}{
"schema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"invoice_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Unique invoice identifier",
},
"invoice_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Date invoice was issued (DD.MMM.YYYY format)",
},
"due_date": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Payment due date (DD.MMM.YYYY format)",
},
"bill_to_party": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Customer name and address receiving the bill",
},
"customer_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Customer reference number",
},
"bill_of_lading_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Shipping bill of lading reference number",
},
"total_amount_due": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Total invoice amount in PHP currency",
},
"vessel_voyage": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Vessel name and voyage number",
},
"port_of_loading": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Port where cargo originated",
},
"port_of_discharge": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Final destination port",
},
"estimated_arrival": map[string]interface{}{
"type": []string{"string", "null"},
"description": "Estimated arrival date (DD-MMM-YYYY format)",
},
"vat_registration_number": map[string]interface{}{
"type": []string{"string", "null"},
"description": "VAT/Tax identification number of billing party",
},
},
},
},
},
},
},
}
fmt.Printf("Deploying \"%s\"…\n", workflow.Name)
state := loadState()
if state.WorkflowID != "" {
fmt.Printf("✓ workflow already provisioned (%s) — updating steps\n", state.WorkflowID)
_, err := apiCall("POST", "/workflows/"+state.WorkflowID, map[string]interface{}{"steps": workflow.Steps})
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
} else {
// Reuse an existing workflow with the same name if one exists.
listResp, err := apiCall("GET", "/workflows?name="+url.QueryEscape(workflow.Name), nil)
if err == nil {
items := listResp.Data
if len(items) == 0 {
items = listResp.Items
}
for _, item := range items {
if m, ok := item.(map[string]interface{}); ok {
if name, ok := m["name"].(string); ok && name == workflow.Name {
if id, ok := m["id"].(string); ok && id != "" {
state.WorkflowID = id
saveState(state)
fmt.Printf("✓ workflow \"%s\" found in your account (%s) — updating steps\n", workflow.Name, id)
_, err := apiCall("POST", "/workflows/"+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 := created.ID
if wfID == "" && created.Workflow != nil {
wfID = created.Workflow.ID
}
if wfID == "" {
fmt.Fprintf(os.Stderr, "Could not read created workflow id from response\n")
os.Exit(1)
}
state.WorkflowID = wfID
saveState(state)
fmt.Printf("+ created workflow (%s)\n", wfID)
}
}
// Deploy the current draft as a new version so the workflow is runnable.
apiCall("POST", "/workflows/"+state.WorkflowID+"/versions", map[string]interface{}{})
fmt.Println("\nDone. Run documents through it with:")
fmt.Printf(" POST %s/workflow_runs { workflow: { id: \"%s\" }, file: { url: \"https://…\" } }\n", API, state.WorkflowID)
fmt.Println("Or open the workflow in the Extend dashboard to review and deploy it.")
}This template processes vendor invoices, capturing billing party information, invoice dates, payment terms, and itemized charges.