# Po Acknowledgment Matcher (`ceeb/po-acknowledgment-matcher`) Actor

- **URL**: https://apify.com/ceeb/po-acknowledgment-matcher.md
- **Developed by:** [ceeb](https://apify.com/ceeb) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## PO Acknowledgment Matcher

A conservative, machine-callable Apify Actor that compares a normalized purchase order with a supplier acknowledgment and returns structured discrepancies.

The V0 goal is narrow: answer **"What did the supplier change?"** without trying to become an ERP, procurement platform, or workflow system.

### Current scope

V0 accepts **normalized JSON** in a standard Actor run or over the Standby HTTP API.

For a standard run, use `example-input.json` as the input. The run compares one
document pair, writes one comparison to its default dataset, and exits. Invalid
input fails without writing a comparison. Standby runs retain the HTTP endpoints
below and return results directly without writing a dataset. No billing event is
implemented in either mode.

It compares:

- purchase-order number
- supplier identity
- currency
- freight and document total
- line number and SKU
- quantity
- unit of measure
- unit price
- requested vs. confirmed delivery date
- acknowledgment line status
- missing PO lines
- extra acknowledgment lines

Matching is deliberately conservative:

- exact line number is preferred
- otherwise an exact unique SKU may match
- candidates are checked across both complete documents before pairing; contested candidates are always `REVIEW`
- the Actor never guesses an ambiguous line match
- numeric and date comparisons are deterministic after normalization

PDF extraction is intentionally deferred until the normalized matcher passes its benchmark suite.

### API

#### Health

```http
GET /
GET /health
```

#### Compare

```http
POST /match
Content-Type: application/json
```

Example request:

```json
{
  "purchase_order": {
    "document_id": "PO-1001",
    "supplier": {"id": "ACME"},
    "currency": "USD",
    "freight": 0,
    "total": 1840,
    "lines": [
      {
        "line_number": "10",
        "sku": "ABC-492",
        "quantity": 100,
        "uom": "EA",
        "unit_price": 18.40,
        "requested_date": "2026-10-12"
      }
    ]
  },
  "acknowledgment": {
    "document_id": "ACK-77",
    "purchase_order_number": "PO-1001",
    "supplier": {"id": "ACME"},
    "currency": "USD",
    "freight": 0,
    "total": 1915,
    "lines": [
      {
        "line_number": "10",
        "sku": "ABC-492",
        "quantity": 100,
        "uom": "EA",
        "unit_price": 19.15,
        "confirmed_date": "2026-11-03",
        "status": "accepted"
      }
    ]
  }
}
```

Example response:

```json
{
  "status": "REVIEW",
  "purchase_order_number": "PO-1001",
  "acknowledgment_id": "ACK-77",
  "summary": {
    "purchase_order_lines": 1,
    "acknowledgment_lines": 1,
    "matched_lines": 0,
    "review_lines": 1,
    "missing_lines": 0,
    "extra_lines": 0,
    "exception_count": 3
  },
  "exceptions": [
    {
      "code": "TOTAL_CHANGED",
      "status": "REVIEW",
      "field": "total",
      "ordered": 1840,
      "acknowledged": 1915,
      "po_line": null,
      "ack_line": null,
      "sku": null,
      "delta": 75.0,
      "delta_pct": 4.0760869565,
      "evidence": {
        "purchase_order": {"path": "purchase_order.total", "value": 1840},
        "acknowledgment": {"path": "acknowledgment.total", "value": 1915}
      }
    }
  ],
  "line_results": []
}
```

The actual response includes every detected exception and every matched/missing/extra line result.

### Optional tolerances

```json
{
  "tolerances": {
    "price_abs": 0.01,
    "price_pct": 0,
    "quantity_abs": 0,
    "date_days": 0
  }
}
```

A price difference is flagged only when it exceeds both the absolute and percentage thresholds.

### Input validation

Both documents must provide a `lines` array of objects, and the PO must contain at least one line. An empty acknowledgment array is valid and reports missing PO lines. Null, empty, and whitespace-only acknowledgment values require review when the corresponding PO value is populated, including freight and total.

Supplied numbers and dates are validated even on unmatched lines. Numbers must be finite; nonzero numeric magnitudes must be at least `1e-100` and less than `1e101`, keeping delta calculations representable in JSON. Tolerances must be nonnegative, and `date_days` must be an integer. Identifiers and text fields accept strings or numbers, not arrays, objects, or booleans.

Malformed JSON or invalid encoding returns HTTP 400; invalid document input returns HTTP 422; bodies over 2 MiB return HTTP 413. HTTP results are returned directly, without writing a dataset.

`MATCH` applies to the supplied PO fields; it does not establish that an upstream extraction captured every required business field.

### Result statuses

- `MATCH` — no configured discrepancy found
- `REVIEW` — one or more discrepancies or ambiguities require attention
- `MISSING_IN_ACK` — a PO line could not be found in the acknowledgment
- `EXTRA_IN_ACK` — an acknowledgment line could not be tied to a PO line

### Safety invariant

The commercial benchmark is asymmetric:

> **A doubtful line may become REVIEW, but it must never become a false MATCH.**

That principle takes precedence over maximizing automatic match rate.

### Development

Run unit tests:

```bash
python -m unittest discover -s tests -v
```

Run the Actor locally with the Apify CLI:

```bash
apify run
```

The Actor keeps the Apify Standby readiness endpoint required by the template.

### V0 benchmark cases

The included tests cover:

- exact match
- changed price
- changed delivery date
- missing line
- extra line
- SKU fallback matching
- ambiguous duplicate SKU
- PO number mismatch
- supplier mismatch
- tolerances
- non-accepted line status
- invalid dates and malformed/non-finite values
- blank acknowledgment fields and missing document amounts
- contested candidates on either side and input-order invariance
- 100-line synthetic documents with price, quantity, and date changes
- HTTP health/readiness, error responses, body limits, and abort cleanup

CI runs the matcher and HTTP tests on Python 3.12 and 3.14 (the Docker runtime). The next publication gate is a private platform benchmark with representative procurement documents and **zero false MATCH results**. Local tests do not substitute for platform validation.

### Not in V0

- OCR
- PDF parsing
- ERP integrations
- user accounts
- dashboards
- write-back to purchasing systems
- automatic approval/rejection decisions
- billing or monetization logic

# Actor input Schema

## `purchase_order` (type: `object`):

Normalized JSON document; see README for line fields. No OCR or purchasing approval.

## `acknowledgment` (type: `object`):

Normalized JSON document; see README for line fields. No OCR or purchasing approval.

## `tolerances` (type: `object`):

Nonnegative price_abs, price_pct, quantity_abs and integer date_days.

## Actor input object example

```json
{
  "purchase_order": {
    "document_id": "PO-1001",
    "supplier": {
      "id": "ACME"
    },
    "currency": "USD",
    "freight": 0,
    "total": 1840,
    "lines": [
      {
        "line_number": "10",
        "sku": "ABC-492",
        "quantity": 100,
        "uom": "EA",
        "unit_price": 18.4,
        "requested_date": "2026-10-12"
      }
    ]
  },
  "acknowledgment": {
    "document_id": "ACK-77",
    "purchase_order_number": "PO-1001",
    "supplier": {
      "id": "ACME"
    },
    "currency": "USD",
    "freight": 0,
    "total": 1840,
    "lines": [
      {
        "line_number": "10",
        "sku": "ABC-492",
        "quantity": 100,
        "uom": "EA",
        "unit_price": 18.4,
        "confirmed_date": "2026-10-12",
        "status": "accepted"
      }
    ]
  },
  "tolerances": {
    "price_abs": 0.01,
    "price_pct": 0,
    "quantity_abs": 0,
    "date_days": 0
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "purchase_order": {
        "document_id": "PO-1001",
        "supplier": {
            "id": "ACME"
        },
        "currency": "USD",
        "freight": 0,
        "total": 1840,
        "lines": [
            {
                "line_number": "10",
                "sku": "ABC-492",
                "quantity": 100,
                "uom": "EA",
                "unit_price": 18.4,
                "requested_date": "2026-10-12"
            }
        ]
    },
    "acknowledgment": {
        "document_id": "ACK-77",
        "purchase_order_number": "PO-1001",
        "supplier": {
            "id": "ACME"
        },
        "currency": "USD",
        "freight": 0,
        "total": 1840,
        "lines": [
            {
                "line_number": "10",
                "sku": "ABC-492",
                "quantity": 100,
                "uom": "EA",
                "unit_price": 18.4,
                "confirmed_date": "2026-10-12",
                "status": "accepted"
            }
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ceeb/po-acknowledgment-matcher").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "purchase_order": {
        "document_id": "PO-1001",
        "supplier": { "id": "ACME" },
        "currency": "USD",
        "freight": 0,
        "total": 1840,
        "lines": [{
                "line_number": "10",
                "sku": "ABC-492",
                "quantity": 100,
                "uom": "EA",
                "unit_price": 18.4,
                "requested_date": "2026-10-12",
            }],
    },
    "acknowledgment": {
        "document_id": "ACK-77",
        "purchase_order_number": "PO-1001",
        "supplier": { "id": "ACME" },
        "currency": "USD",
        "freight": 0,
        "total": 1840,
        "lines": [{
                "line_number": "10",
                "sku": "ABC-492",
                "quantity": 100,
                "uom": "EA",
                "unit_price": 18.4,
                "confirmed_date": "2026-10-12",
                "status": "accepted",
            }],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("ceeb/po-acknowledgment-matcher").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "purchase_order": {
    "document_id": "PO-1001",
    "supplier": {
      "id": "ACME"
    },
    "currency": "USD",
    "freight": 0,
    "total": 1840,
    "lines": [
      {
        "line_number": "10",
        "sku": "ABC-492",
        "quantity": 100,
        "uom": "EA",
        "unit_price": 18.4,
        "requested_date": "2026-10-12"
      }
    ]
  },
  "acknowledgment": {
    "document_id": "ACK-77",
    "purchase_order_number": "PO-1001",
    "supplier": {
      "id": "ACME"
    },
    "currency": "USD",
    "freight": 0,
    "total": 1840,
    "lines": [
      {
        "line_number": "10",
        "sku": "ABC-492",
        "quantity": 100,
        "uom": "EA",
        "unit_price": 18.4,
        "confirmed_date": "2026-10-12",
        "status": "accepted"
      }
    ]
  }
}' |
apify call ceeb/po-acknowledgment-matcher --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ceeb/po-acknowledgment-matcher"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/CC4eod3t9TqnPMrV5/builds/hwGRkD0p3zl5pPCIp/openapi.json
