Invoice Intelligence
Reconcile an invoice against its purchase order and receipts, and find discrepancies before payment approval.
Endpoint
POST https://kovca.io/api/v1/invoices/checkAuthenticate with your API key in the Authorization: Bearer header. Send JSON with Content-Type: application/json; bodies are limited to 16 KB.
The API checks the invoice you send against the purchase order and receipts you send. All arithmetic is exact decimal arithmetic in code. Interpretation is used only to decide which purchase-order line a differently worded invoice line describes. It never authorizes or initiates payments.
Request body
| Field | Type | Description |
|---|---|---|
| invoice.number | string, required | Up to 64 characters. |
| invoice.vendor | { name, id? }, required | Vendor name, and your vendor ID if you have one. |
| invoice.currency | string, required | ISO 4217 code, such as USD. |
| invoice.date | string, optional | YYYY-MM-DD. |
| invoice.lines | object[], required | 1 to 50 lines: description (required), quantity, unit_price, and optional line_id, sku, amount, unit. |
| invoice.subtotal, invoice.tax, invoice.total | decimal, optional | Checked against the lines when supplied. The total is checked only when tax is supplied. |
| purchase_order | object, optional | number, currency, and 1 to 50 lines: description, quantity, unit_price, and optional line_id, sku, unit. |
| receipts | object[], optional | Quantities received: { po_line_id, quantity_received }. po_line_id refers to a purchase-order line's line_id. |
| known_invoices | object[], optional | Up to 50 previously received invoices: { number, vendor_id?, vendor_name? }, used for duplicate detection only. |
| tolerances | object, optional | unit_price_percent, unit_price_absolute, quantity_percent, amount_absolute. Defaults: exact prices and quantities, 0.01 for amounts (rounding). |
Example request
{
"invoice": {
"number": "INV-20931",
"vendor": {
"name": "Northwind Supply",
"id": "V-1042"
},
"currency": "USD",
"lines": [
{
"sku": "NW-4410",
"description": "Nitrile gloves, large, box of 100",
"quantity": 40,
"unit_price": "12.50",
"amount": "500.00"
},
{
"description": "Hand sanitizer gel 500ml pump bottle",
"quantity": 24,
"unit_price": "4.10",
"amount": "98.40"
}
],
"subtotal": "598.40",
"tax": "47.87",
"total": "646.27"
},
"purchase_order": {
"number": "PO-7781",
"currency": "USD",
"lines": [
{
"line_id": "1",
"sku": "NW-4410",
"description": "Gloves, nitrile, L (100/box)",
"quantity": 40,
"unit_price": "12.50"
},
{
"line_id": "2",
"description": "Sanitizer, gel, 500 ml pump",
"quantity": 24,
"unit_price": "3.95"
}
]
}
}Response
200 OK
{
"request_id": "req_2bWq9Lm4Ks7TzXcN",
"status": "discrepancies_found",
"recommended_action": "reject_for_correction",
"issues": [
{
"type": "unit_price_mismatch",
"invoice_line": 1,
"po_line": 1,
"expected": "3.95",
"actual": "4.10",
"explanation": "The unit price differs from the purchase order price.",
"evidence": [
"invoice.lines[1]",
"purchase_order.lines[1]"
]
}
],
"matched_lines": [
{
"invoice_line": 0,
"po_line": 0,
"method": "identifier"
},
{
"invoice_line": 1,
"po_line": 1,
"method": "description"
}
],
"unmatched_lines": [],
"missing_information": []
}| Field | Type | Description |
|---|---|---|
| request_id | string | Unique ID for this request. |
| status | "matched" | "discrepancies_found" | "requires_review" | "insufficient_information" | Overall result. See below. |
| recommended_action | "approve_for_processing" | "request_information" | "manual_review" | "reject_for_correction" | The suggested next step. Never an instruction to pay. |
| issues | object[] | type, optional invoice_line and po_line (zero-based indexes), expected and actual values as strings, a fixed explanation, and evidence (request fields). |
| matched_lines | object[] | { invoice_line, po_line, method }. method is "identifier" | "description". |
| unmatched_lines | number[] | Invoice lines with no corresponding purchase-order line. |
| missing_information | "purchase_order" | "exchange_rate"[] | purchase_order when none was sent; exchange_rate when currencies differ. |
How an invoice is checked
- Duplicates: the invoice number and vendor are compared with
known_invoices. - Arithmetic: each line amount, the subtotal, and the total (when tax is given) are recomputed with exact decimals.
- Line matching: lines with the same
skuorline_idare paired in code. Remaining lines are matched by description. A description match must be confident and can't be claimed by two invoice lines; otherwise it'sambiguous_line_match. - Matched lines: quantities are compared with the order and receipts, units must agree, and unit prices are compared in the same currency.
Statuses (first match wins)
requires_review: A possible duplicate, an ambiguous line match, or a currency mismatch. Action: manual_review.discrepancies_found: A quantity, price, or amount discrepancy (action: reject_for_correction), or an invoice line with no order line (action: manual_review).insufficient_information: No purchase order and no other problems. Action: request_information.matched: Every line matched and agreed. Action: approve_for_processing.
Issue types
quantity_mismatch: A line bills more than was ordered or received (beyond quantity_percent), or uses a different unit than the order line.unit_price_mismatch: A line's unit price differs from the order price beyond the price tolerances.amount_mismatch: A line amount isn't quantity × unit price, the subtotal isn't the sum of the lines, or the total isn't subtotal plus tax.currency_mismatch: The invoice and purchase order currencies differ. Prices aren't compared.unmatched_line: No purchase-order line describes this invoice line.ambiguous_line_match: It's unclear which purchase-order line an invoice line describes. A person should decide.missing_purchase_order: No purchase order was supplied; only the invoice's own arithmetic and duplicates were checked.possible_duplicate: known_invoices has the same invoice number from the same vendor.
Without a purchase order
Request
{
"invoice": {
"number": "INV-88",
"vendor": {
"name": "Acme Facilities"
},
"currency": "EUR",
"lines": [
{
"description": "Quarterly HVAC maintenance visit",
"quantity": 1,
"unit_price": "450.00",
"amount": "450.00"
}
],
"total": "535.50",
"tax": "85.50"
}
}Code
curl https://kovca.io/api/v1/invoices/check \
-H "Authorization: Bearer $KOVCA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"invoice": {
"number": "INV-20931",
"vendor": {
"name": "Northwind Supply",
"id": "V-1042"
},
"currency": "USD",
"lines": [
{
"sku": "NW-4410",
"description": "Nitrile gloves, large, box of 100",
"quantity": 40,
"unit_price": "12.50",
"amount": "500.00"
},
{
"description": "Hand sanitizer gel 500ml pump bottle",
"quantity": 24,
"unit_price": "4.10",
"amount": "98.40"
}
],
"subtotal": "598.40",
"tax": "47.87",
"total": "646.27"
},
"purchase_order": {
"number": "PO-7781",
"currency": "USD",
"lines": [
{
"line_id": "1",
"sku": "NW-4410",
"description": "Gloves, nitrile, L (100/box)",
"quantity": 40,
"unit_price": "12.50"
},
{
"line_id": "2",
"description": "Sanitizer, gel, 500 ml pump",
"quantity": 24,
"unit_price": "3.95"
}
]
}
}'Errors
400 INVALID_REQUEST: a field is missing, too long, the wrong type, or not allowed. The message names the field.400 INVALID_REQUESTalso covers amounts that aren't plain decimals (for example"$12"or1e3) and currencies that aren't 3-letter codes.413 PAYLOAD_TOO_LARGE: the body is larger than 16 KB.401 UNAUTHORIZED,429 RATE_LIMITED,503 SERVICE_UNAVAILABLE,504 TIMEOUT: see Errors and limits.
Limitations
- Matching is one purchase order per request. Split invoices spanning several orders.
- No currency conversion. Different currencies are flagged, never converted.
- Duplicates are detected only against known_invoices you supply.
- Billing less than ordered is treated as a partial invoice, not a discrepancy.
- Request data is processed to produce the result and is not stored by Kovca.