# Poland VAT Whitelist Checker & Supplier Monitor (`automa-flow/poland-vat-whitelist-monitor`) Actor

Bulk-check up to 3,000 Polish suppliers against the official VAT White List (Biała Lista): verify VAT status and registered bank accounts, preserve official request IDs, and monitor deterministic supplier changes between scheduled runs. Failed checks never become false absences.

- **URL**: https://apify.com/automa-flow/poland-vat-whitelist-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Business, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 resolved supplier checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Poland VAT Whitelist Checker & Supplier Monitor

Check VAT status and registered bank accounts today, then run the same supplier
portfolio again to receive deterministic, webhook-ready change events.

> **Unofficial community Actor.** This Actor is not affiliated with, sponsored
> by, or endorsed by the Polish Ministry of Finance (MF) or National Revenue
> Administration (KAS). It reads their public VAT White List API.

Bulk-check a Polish supplier portfolio against the official Ministry of Finance
VAT White List (Biała Lista), verify an optional NIP + bank-account pair, and
monitor deterministic VAT or registered-account changes between runs.

The Actor is built for accounting, AP, procurement, ERP and supplier-compliance
workflows. It uses the official JSON API over direct HTTP: no browser, CAPTCHA,
login, proxy, proxy rotation, VIES, KRS/CEIDG enrichment or private database.

This is not tax or legal advice. The Actor reports what the official register
returned for the requested date and does not decide whether a payment is lawful
or recommend a tax procedure.

### Use from AI agents with MCP

Connect the public Actor directly at
`https://mcp.apify.com?tools=automa-flow/poland-vat-whitelist-monitor`. Ask your
agent to verify a supplier or bank account and preserve the official request ID,
requested date, and explicit per-item status in its answer.

#### What you can do

1. **Bulk Polish VAT supplier verification** — reconcile up to 3,000 NIPs in a
   single run with one explicit output row per input.
2. **Supplier master-data monitoring** — reuse a `monitorId` and retain only the
   last verified normalized state in Apify KVS.
3. **Weekly VAT-status change detection** — receive exact
   `vat_status_changed` and registration-state/date events.
4. **Bank-account change monitoring** — compare sorted account hashes and emit
   `bank_account_added` / `bank_account_removed` without keeping raw accounts in
   monitor state.
5. **Pre-payment verification** — include `bankAccount` to run the official
   NIP + account `TAK`/`NIE` method and retain both official request IDs.

#### Why use the supplier monitor instead of a one-off checker?

The default Dataset always keeps one explicit reconciliation row per input, so
AP and supplier-master workflows can join results without guessing which NIP was
lost. In monitor mode, only successful official observations advance state;
source failures stay visible and cannot manufacture a VAT/account change. The
`changes` view is already flattened for scheduled-run webhooks.

#### Input

```json
{
  "records": [
    {
      "nip": "PL 526-025-02-74",
      "reference": "invoice-2026-0042",
      "bankAccount": "PL73 1111 1111 1111 1111 1111 1111"
    }
  ],
  "mode": "monitor",
  "monitorId": "weekly-ap-watchlist",
  "onlyChanges": false,
  "includeFullBankAccounts": false
}
```

- `records` is required and accepts 1–3,000 rows.
- `nip` is normalized from PL/spaces/dashes and must pass the Polish checksum.
- `reference` is optional, at most 128 characters, and is echoed unchanged.
- `bankAccount` is optional Polish NRB/PL IBAN. It is checksum-validated before
  any request and is never logged or included in an error URL.
- `mode` defaults to `snapshot`. `monitor` requires a safe, stable `monitorId`.
- `asOfDate` defaults to the current Europe/Warsaw date.
  The official register currently permits dates in the current year and the
  five preceding calendar years.
- `onlyChanges=true` minimizes unchanged profile details, but deliberately keeps
  one explicit status row per input so batch reconciliation cannot silently lose
  suppliers. Use the `changes` Dataset view for event-only webhooks.
- `includeFullBankAccounts` defaults to false. Enable it only when the workflow
  truly needs full registered accounts.

Concurrency, retries, delays and proxy internals are intentionally not public
inputs. Official quotas, rather than CPU, are the limiting resource.

#### Output and failure semantics

Each input row produces one row with:

- stable `source`, normalized `source_id`, `source_url`, UTC `scraped_at`,
  `schema_version` and semantic `fingerprint`;
- `SUCCESS`, `NOT_FOUND`, `PARTIAL`, `FAILED` or `INVALID_INPUT`;
- normalized `ACTIVE`, `EXEMPT`, `NOT_REGISTERED` or `UNKNOWN` VAT status;
- masked registered accounts and deterministic account hashes;
- optional authoritative `account_check` with last four digits only;
- the Ministry search request ID and optional account-check request ID;
- `changed`, `change_type` and deterministic `changes[]` in monitor mode;
- a safe structured `error` on unverified work.

`NOT_FOUND` is only produced after HTTP 200 with a valid response entry and an
empty `subjects` array. Timeout, HTTP 408/429/5xx, quota exhaustion, nightly
maintenance, malformed HTTP-200 JSON/schema and account-check failure are never
converted to absence. A failed supplier does not terminate valid siblings.

The run itself fails, after publishing explicit rows, only when the whole run is
untrustworthy: every official search batch failed, the official allowance
prevented every check, the HTTP-200 response envelope materially changed, or a
billing limit changed while publishing.

A partial outcome succeeds instead. If the official allowance stops some checks
after others resolved, the run succeeds, the unattempted suppliers carry
explicit `SOURCE_QUOTA` rows, and the status message names how many were
stopped. This keeps the `ACTOR.RUN.SUCCEEDED` webhook firing for scheduled
monitors, so a cycle that did contain verified changes is never skipped
silently. Filter `status` in the `diagnostics` view to see what was missed.

One supplier the parser cannot read, such as an unrecognized VAT status or a
checksum-invalid registered account, is an isolated `MALFORMED_SUBJECT` row for
that supplier only. It does not fail the run and does not stop the remaining
suppliers from being checked.

#### Monitoring

State is namespaced by `monitorId + normalized NIP` in an Apify named KVS. It
contains only the last verified semantic observation, its fingerprint and
account hashes. Raw source payloads and full bank accounts are never retained.

The first verified monitor run is a baseline (`changed=false`). Later verified
runs can emit:

- `vat_status_changed`
- `registration_state_changed`
- `registration_date_changed`
- `removal_date_changed`
- `restoration_date_changed`
- `bank_account_added`
- `bank_account_removed`
- supplier name, REGON or KRS changes already present in the official response

A failed profile never overwrites good state. If the supplier profile succeeds
but its requested account check fails, the row is `PARTIAL`; the independently
verified profile can advance the VAT/account-set baseline because the failed
field is not part of that baseline.

#### Official source limits and update window

The current official production documentation is API v1.6:
https://www.gov.pl/web/kas/api-wykazu-podatnikow-vat

- `search`: 100 requests/day, at most 30 subjects/request — a practical maximum
  of approximately 3,000 searched NIPs/day.
- `check`: 5,000 subjects/day.
- Exhausting either allowance can block both API methods and the public search
  until 00:00. The Actor stops remaining work and never changes egress identity
  to multiply that allowance.
- A retry spends one request from the same 100/day search allowance, so the
  Actor sizes its retry budget from what the allowance can still fund. At the
  3,000-supplier maximum all 100 requests are committed to first attempts and
  the run performs no retries: a transient upstream failure then costs that one
  batch of 30 an explicit `FAILED` row instead of consuming the allowance a
  later batch still needs. Below roughly 2,700 suppliers there is real retry
  headroom.
- The register is updated once on working days. The Ministry warns of possible
  disruption during 22:00–00:00 Europe/Warsaw, so retries are suppressed in
  that window.
- The official daily flat file is the documented option for substantially
  larger verification volumes; it is outside this MVP.

Because allowances can be enforced by egress IP, a representative 300-NIP
direct-HTTP run from Apify is a pre-publication gate. It was run on 2026-09-03:
300 suppliers over 10 search batches resolved with a 100% useful-output rate, no
HTTP 429, no quota collision and one transient retry, using shared Apify egress
without a browser, CAPTCHA, login or proxy. A single run still consumes its
share of the official 100-request daily search allowance, and a retry consumes
one more, so schedule large portfolios accordingly.

#### Privacy

The Ministry response can include PESEL-capable fields, representatives,
authorized clerks, partners and personal addresses. The parser uses an explicit
allowlist and never returns or stores those fields. It also never stores raw
responses. Registered accounts are masked by default; monitor state uses only
SHA-256 values. The full-account output is an explicit opt-in and full accounts
never appear in logs.

#### Pay-per-event pricing

One `supplier_checked` event at **$0.005 per resolved supplier** ($5 per 1,000),
with platform usage included and no separate start fee. A supplier can generate
at most one event, even when an extra official account request is needed.

Billable: complete `SUCCESS` and authoritative `NOT_FOUND` rows, including a
supplier that monitor mode reports as unchanged. A scheduled monitor run
therefore costs the same per supplier as the first run; `onlyChanges` reduces
row size, not price.

Free: invalid input, retries, `PARTIAL`, `FAILED`, quota/source failures,
billing-limit skips and KVS operations.

| Resolved suppliers | Actor charge |
| ---: | ---: |
| 100 | $0.50 |
| 1,000 | $5.00 |
| 3,000 | $15.00 |

Set `maxTotalChargeUsd` on a run or Task to cap spend. Suppliers that the cap
cannot cover are never contacted and are published as explicit
`MAX_TOTAL_CHARGE_REACHED` rows, so a capped run still reconciles one row per
input and never advances monitor state for work it did not pay for.

#### Run through the Apify API

Keep the API token in an environment secret; never paste it into Actor input or
source control.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/<ACTOR_ID>/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "records": [{"nip": "5260250274", "reference": "supplier-001"}],
    "mode": "snapshot"
  }'
```

#### Schedule and webhook example

Create an Apify Task with a stable monitor input, then schedule it weekly after
the nightly source window, for example every Monday at 06:00 Europe/Warsaw:

```json
{
  "records": [
    {"nip": "5260250274", "reference": "supplier-001"},
    {"nip": "7000000009", "reference": "supplier-002"}
  ],
  "mode": "monitor",
  "monitorId": "weekly-ap-watchlist",
  "onlyChanges": true
}
```

Subscribe a webhook to the Task's `ACTOR.RUN.SUCCEEDED` event. From the webhook
payload's default Dataset ID, fetch the flattened event view:

```text
GET https://api.apify.com/v2/datasets/<DATASET_ID>/items?view=changes&clean=true
```

The view is empty when there are no deterministic changes. Failure/status rows
remain available in the default Dataset. The `diagnostics` view projects status
and error columns for every row; filter `status` client-side when selecting only
failures.

#### Local verification

Offline fixture demonstration:

```bash
uv run python actors/poland-vat-whitelist-monitor/scripts/local_run.py
```

Manual direct-source smoke (not used by CI):

```bash
uv run python actors/poland-vat-whitelist-monitor/scripts/smoke_test.py 5260250274
```

Full repository gate:

```bash
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy
```

The dated source evidence, legal/privacy review, pending hosted gate and economic
guardrail are recorded in `experiments/poland-vat-whitelist/README.md`.

# Actor input Schema

## `records` (type: `array`):

One row per supplier. NIP accepts PL, spaces and dashes and is checksum-validated before any request. reference is echoed unchanged. bankAccount accepts checksum-valid Polish NRB or PL IBAN and is never written to logs.

## `mode` (type: `string`):

snapshot returns the current official observation. monitor compares with the last verified state for monitorId + NIP; its first successful run is a baseline.

## `asOfDate` (type: `string`):

Optional YYYY-MM-DD date supported by the official register. Defaults to today's Europe/Warsaw date.

## `monitorId` (type: `string`):

Required in monitor mode. A stable 1-64 character watchlist key using letters, digits, dots, underscores or hyphens.

## `onlyChanges` (type: `boolean`):

In monitor mode, unchanged suppliers still emit one explicit reconciliation row, but repeat profile details are omitted. Changed, baseline and failed rows remain complete.

## `includeFullBankAccounts` (type: `boolean`):

Off by default. Enable only when your workflow requires full registered account numbers. Monitor state still retains hashes only and logs never contain full accounts.

## Actor input object example

```json
{
  "records": [
    {
      "nip": "5260250274",
      "reference": "supplier-001"
    },
    {
      "nip": "PL 526-025-02-74",
      "reference": "invoice-2026-0042",
      "bankAccount": "PL73 1111 1111 1111 1111 1111 1111"
    }
  ],
  "mode": "snapshot",
  "onlyChanges": false,
  "includeFullBankAccounts": false
}
```

# Actor output Schema

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

Exactly one status row per input supplier, including invalid and failed entries.

## `changes` (type: `string`):

Webhook-ready flattened change events from monitor mode.

## `diagnostics` (type: `string`):

Failure-focused columns for all rows; filter the status field client-side.

## `runSummary` (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 = {
    "records": [
        {
            "nip": "5260250274",
            "reference": "supplier-001"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/poland-vat-whitelist-monitor").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 = { "records": [{
            "nip": "5260250274",
            "reference": "supplier-001",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/poland-vat-whitelist-monitor").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 '{
  "records": [
    {
      "nip": "5260250274",
      "reference": "supplier-001"
    }
  ]
}' |
apify call automa-flow/poland-vat-whitelist-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/poland-vat-whitelist-monitor"
        }
    }
}

```

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/g1rV2i18sPQ8iUpSh/builds/9rgxXjKLWzgtXTUYx/openapi.json
