End-to-End: Invoice Reconciliation
This guide walks through building a complete invoice reconciliation workflow from scratch. By the end, you'll have a pipeline that matches invoices to payments, investigates exceptions with an AI agent, routes edge cases to human reviewers, and logs everything to an audit table: running on a daily schedule.
Prerequisites
You need a running Hyphen instance and curl. All examples use X-Org-Id: acme-corp.
Step 1: Register Org Config
Store the API keys your workflow actions will use:
curl -X POST https://your-hyphen.example.com/org-config \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{ "key": "api:llm_api_key", "value": "sk-your-openai-key" }'
curl -X POST https://your-hyphen.example.com/org-config \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{ "key": "api:erp_token", "value": "your-erp-api-token" }'
Config values are encrypted at rest and referenced in workflows with the orgconfig: prefix.
Step 2: Register HTTP Actions
Register the actions the AI agent will use as tools:
# Look up a purchase order from your ERP
curl -X POST https://your-hyphen.example.com/actions \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"action_name": "lookup_purchase_order",
"kind": "http",
"url": "https://erp.acme.com/api/purchase-orders/{{po_number}}",
"http_method": "GET",
"headers": { "Authorization": "Bearer orgconfig:api:erp_token" },
"passthrough": true
}'
# Search payment history by vendor
curl -X POST https://your-hyphen.example.com/actions \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"action_name": "check_payment_history",
"kind": "db",
"datasource": "orgconfig:db:finance_pg",
"query": "SELECT * FROM payments WHERE vendor_id = $1 AND payment_date BETWEEN $2 AND $3",
"params": ["@input.vendor_id", "@input.start_date", "@input.end_date"],
"passthrough": true
}'
# Check for duplicate invoices
curl -X POST https://your-hyphen.example.com/actions \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"action_name": "search_duplicate_invoices",
"kind": "db",
"datasource": "orgconfig:db:finance_pg",
"query": "SELECT * FROM invoices WHERE amount = $1 AND vendor_id = $2 AND invoice_date BETWEEN $3 AND $4 AND invoice_id != $5",
"params": ["@input.amount", "@input.vendor_id", "@input.start_date", "@input.end_date", "@input.invoice_id"],
"passthrough": true
}'
Step 3: Create the Matcher Workflow
Start simple: just the matcher step:
curl -X POST https://your-hyphen.example.com/workflows \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice_reconciliation_v1",
"definition": {
"actions": [
{
"type": "matcher",
"properties": {
"left": "@input.invoices",
"right": "@input.payments",
"matchOn": ["po_number", "vendor_id"],
"tolerance": 50,
"dateWindowDays": 5,
"fuzzyThreshold": 85,
"descriptionKey": "vendor_name",
"outputMatched": "reconciled",
"outputUnmatchedLeft": "unmatched_invoices",
"outputUnmatchedRight": "unmatched_payments"
}
}
]
}
}'
Response:
{ "id": "wfl-123e4567-e89b-12d3-a456-426614174000", "name": "invoice_reconciliation_v1" }
Step 4: Execute with Sample Data
curl -X POST https://your-hyphen.example.com/workflows/wfl-123e4567-e89b-12d3-a456-426614174000/execute \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"input": {
"invoices": [
{ "invoice_id": "INV-001", "po_number": "PO-100", "vendor_id": "V-50", "vendor_name": "Acme Corp", "amount": 1000.00, "date": "2026-01-15" },
{ "invoice_id": "INV-002", "po_number": "PO-101", "vendor_id": "V-51", "vendor_name": "Beta LLC", "amount": 2500.00, "date": "2026-01-16" },
{ "invoice_id": "INV-003", "po_number": "PO-999", "vendor_id": "V-52", "vendor_name": "Gamma Inc", "amount": 750.00, "date": "2026-01-17" }
],
"payments": [
{ "payment_id": "PAY-001", "po_number": "PO-100", "vendor_id": "V-50", "vendor_name": "ACME Corporation", "amount": 1000.00, "date": "2026-01-18" },
{ "payment_id": "PAY-002", "po_number": "PO-101", "vendor_id": "V-51", "vendor_name": "Beta LLC", "amount": 2450.00, "date": "2026-01-20" }
]
}
}'
Check the result:
curl https://your-hyphen.example.com/runs/run-123e4567-e89b-12d3-a456-426614174000/status \
-H "X-Org-Id: acme-corp"
You should see INV-001 matched to PAY-001 (exact match), INV-002 matched to PAY-002 ($50 difference within $50 tolerance), and INV-003 as an unmatched exception (no matching PO-999 payment).
Using uploaded files instead of inline data. For production, upload your datasets to Document Storage and reference them with the doc: prefix:
# Upload invoices CSV
curl -X POST https://your-hyphen.example.com/documents -H "X-Org-Id: acme-corp" \
-F "[email protected]" -F 'tags=["invoices"]'
# → { "document": { "id": "doc_inv123..." } }
# Execute with doc: references
curl -X POST https://your-hyphen.example.com/workflows/wfl-123e4567-e89b-12d3-a456-426614174000/execute \
-H "X-Org-Id: acme-corp" -H "Content-Type: application/json" \
-d '{ "input": { "invoices": "doc:doc_inv123", "payments": "doc:doc_pay456" } }'
This avoids embedding large datasets in API payloads and enables webhook-triggered automation when new files are uploaded.
Step 5: Add the Agent Step
Foreach runs inline-safe work, so this pattern first persists the deterministic matcher exceptions and then gives one bounded agent a fixed read tool for the current run.
Create ap_reconciliation_cases with run_id, invoice_id, vendor_name, amount, and status fields, then register the read tool:
curl -X POST https://your-hyphen.example.com/actions \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"action_name": "read_ap_reconciliation_cases",
"kind": "custom-table",
"description": "Read AP reconciliation exceptions for the current run.",
"properties": {
"table": "ap_reconciliation_cases",
"operation": "read",
"where": { "run_id": "@__run_id" },
"limit": 1000
}
}'
Update the workflow to persist and investigate exceptions:
curl -X PUT https://your-hyphen.example.com/workflows/wfl-123e4567-e89b-12d3-a456-426614174000 \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice_reconciliation_v2",
"definition": {
"actions": [
{
"type": "matcher",
"properties": {
"left": "@input.invoices",
"right": "@input.payments",
"matchOn": ["po_number", "vendor_id"],
"tolerance": 50,
"dateWindowDays": 5,
"fuzzyThreshold": 85,
"descriptionKey": "vendor_name",
"outputMatched": "reconciled",
"outputUnmatchedLeft": "unmatched_invoices",
"outputUnmatchedRight": "unmatched_payments"
}
},
{
"type": "loop",
"filter": {
"condition": { "greaterThan": [{ "length": "@unmatched_invoices" }, 0] }
},
"properties": {
"mode": "foreach",
"items_path": "@unmatched_invoices",
"item_variable_name": "exception",
"actions_to_execute": [
{
"type": "custom-table",
"properties": {
"table": "ap_reconciliation_cases",
"operation": "upsert",
"key_fields": ["run_id", "invoice_id"],
"keys": ["run_id", "invoice_id", "vendor_name", "amount", "status"],
"values": ["@__run_id", "@exception.invoice_id", "@exception.vendor_name", "@exception.amount", "pending_review"]
}
}
],
"max_concurrency": 5,
"failure_strategy": "fail_fast",
"collect_results": false,
"result_key": "exception_case_writes"
}
},
{
"type": "loop",
"filter": {
"condition": { "greaterThan": [{ "length": "@unmatched_invoices" }, 0] }
},
"properties": {
"mode": "react",
"objective": "Read the current run's AP reconciliation cases. Investigate each invoice using the declared tools, cite the evidence returned, and recommend the next action. Do not approve a write-off.",
"tools": [
{ "type": "action", "name": "read_ap_reconciliation_cases" },
{ "type": "action", "name": "lookup_purchase_order" },
{ "type": "action", "name": "check_payment_history" },
{ "type": "action", "name": "search_duplicate_invoices" }
],
"max_iterations": 12,
"on_stuck": { "iterations": 3, "action": "retry_with_hint", "hint": "Complete with a low-confidence recommendation for human review when evidence is insufficient." },
"result_key": "all_investigations"
}
}
]
}
}'
Step 6: Add Approval for Write-Offs
Add a PbotApproval step so a human reviews the agent's findings before any write-offs:
Add this step after the agent in the actions array:
{
"type": "PbotApproval",
"filter": {
"condition": { "greaterThan": [{ "length": "@unmatched_invoices" }, 0] }
},
"properties": {
"comment": "{{unmatched_invoices.length}} exceptions investigated. Review AI findings.",
"request_payload": {
"reconciled_count": "@reconciled.length",
"investigations": "@all_investigations",
"unmatched_payments": "@unmatched_payments"
}
}
}
When the workflow pauses, submit the approval:
curl -X POST https://your-hyphen.example.com/approvals/run-123e4567-e89b-12d3-a456-426614174000/2 \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"approved": true,
"comment": "Reviewed. Write off INV-003 as vendor error."
}'
Step 7: Add Audit Logging
Add a custom-table step to persist the reconciliation results:
{
"type": "custom-table",
"properties": {
"table": "reconciliation_log",
"operation": "write",
"keys": ["run_id", "run_date"],
"values": ["@__run_id", "@now"],
"fields": {
"total_invoices": "@input.invoices.length",
"auto_reconciled": "@reconciled.length",
"exceptions": "@unmatched_invoices.length",
"status": "completed"
}
}
}
First, create the table:
curl -X POST https://your-hyphen.example.com/custom-tables \
-H "X-Org-Id: acme-corp" \
-H "Content-Type: application/json" \
-d '{
"name": "reconciliation_log",
"fields": [
{ "name": "run_id", "type": "text", "required": true },
{ "name": "run_date", "type": "timestamptz", "required": true },
{ "name": "total_invoices", "type": "integer" },
{ "name": "auto_reconciled", "type": "integer" },
{ "name": "exceptions", "type": "integer" },
{ "name": "status", "type": "text", "required": true }
]
}'
Step 8: Schedule for Daily Execution
Add a schedule block to the workflow definition:
{
"name": "invoice_reconciliation_v3",
"definition": {
"schedule": {
"every": "1d",
"at": "02:00",
"timezone": "America/New_York"
},
"actions": [ ... ]
}
}
The workflow now runs automatically at 2 AM Eastern every day.
When you have three legs
Invoice-to-payment is two-way. When a purchase order, its receipts, and its invoices all have to agree before a payment is released, use the N-way match step instead of the matcher. It groups two to four datasets on a shared key, sums additive fields within each group, and classifies every group as full, missing_leg, or variance:
{
"type": "nway_match",
"properties": {
"sets": [
{ "key": "purchase_order", "records": "@input.purchase_order_rows" },
{ "key": "receipt", "records": "@input.receipt_rows" },
{ "key": "invoice", "records": "@input.invoice_rows" }
],
"matchOn": ["authorization_line_id"],
"tolerances": { "line_amount": { "type": "currency", "value": 0.01 } },
"exceptionTable": "nway_match_exceptions"
}
}
The rest of the pipeline follows the same shape. Let exceptionTable persist the non-full groups, then bind a read-only custom-table action to those current-run rows for the agent. The shipped case CTR-11 packages that pattern in Process Studio.
What You Built
A complete reconciliation pipeline that:
- Matches invoices to payments on PO number, vendor ID, amount (within $50), and date (within 5 days)
- Investigates unmatched exceptions using an AI agent with access to your ERP and payment database
- Routes edge cases to a human reviewer with source records, tool observations, and the model's stated rationale
- Logs every run to an audit table for compliance and trend analysis
- Runs automatically on a daily schedule
For the full production-ready version with all steps combined, see the AP Invoice Reconciliation template.