# EU Tariff Quota Balance Monitor (`automa-flow/eu-tariff-quota-balance-monitor`) Actor

Track EU tariff quota remaining balances from the European Commission's weekly QUOTA reports. Look up quota order numbers, export published balances, and detect critical, exhausted or reopened quotas between runs. JSON/CSV output for customs and procurement workflows.

- **URL**: https://apify.com/automa-flow/eu-tariff-quota-balance-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

$10.00 / 1,000 conclusive quota observations

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

## EU Tariff Quota Balance Monitor

Look up **EU tariff quota balances by quota order number**, export them as
structured JSON or CSV, and detect what changed since your last run.

Give it `09.0006` and it returns the goods description, origin, validity period,
opened volume, amount already imported, **remaining balance**, unit, last import
date and whether the Commission has flagged the quota critical or exhausted.
Give it a hundred order numbers and it returns a hundred answers, each with its
own status. Schedule it and each run tells you which balances moved, which
quotas went critical, which ran out and which reopened.

Data comes from the European Commission's own weekly QUOTA reports, published by
DG TAXUD in the public CIRCABC group *TARIC and Quota data and information*. The
Actor never touches the DDS2 consultation screens, which the Commission's
`robots.txt` closes to automated clients.

### What it is for

- check the remaining balance on a tariff quota before committing to a shipment;
- watch a portfolio of quota order numbers and get alerted when one goes
  critical, is exhausted or reopens;
- pull every published EU tariff quota balance into a spreadsheet, warehouse or
  ERP in one run;
- feed landed-cost, customs or procurement automation with a machine-readable
  quota feed instead of a screen-scrape;
- keep a dated, append-only history of quota state without running a database.

Typical users: EU importers, customs brokers, freight forwarders,
trade-compliance teams, and developers building customs or landed-cost
workflows.

### Example output

One row per published quota. Run the sample input and `09.0006` returns two rows,
because the Commission publishes a separate balance per validity period:

```json
{
  "source": "eu_taxud_quota",
  "source_id": "090006:2026-06-16:2027-02-14:3da3002f",
  "source_url": "https://circabc.europa.eu/ui/group/0e5f18c2-4b2f-42e9-aed4-dfe50ae1263b",
  "order_number": "090006",
  "source_order_number": "09.0006",
  "description": "Herrings",
  "sub_quota": null,
  "origin": "E.O.",
  "valid_from": "2026-06-16",
  "valid_to": "2027-02-14",
  "period_state": "OPEN",
  "status": "AVAILABLE",
  "critical": false,
  "critical_since": null,
  "initial_volume": 33496000,
  "unit": "Kilograms",
  "imported_volume": 6774668.701,
  "remaining": 26721331.299,
  "last_import_date": "2026-09-01",
  "source_updated_at": "2026-09-04T08:38:58Z",
  "scraped_at": "2026-09-04T16:10:00Z",
  "result_state": "SUCCESS",
  "changed": true,
  "change_types": ["FIRST_SEEN"],
  "previous": null,
  "fingerprint": "sha256:...",
  "schema_version": 1
}
```

`examples/sample_output.json` is a real run of `examples/sample_input.json`,
including the exhausted 2025 period of the same order number and a confirmed
not-found lookup.

### Batch lookup

```json
{
  "quotaOrderNumbers": ["09.0006", "090013", "09.0159", "099524"],
  "detectChanges": false
}
```

- Both written forms are accepted: `090006` and `09.0006`. Leading zeroes are
  preserved, and digits are never silently altered. Only ASCII digits `0-9` are
  accepted; Unicode lookalikes are returned as free `INVALID_INPUT` diagnostics.
- Duplicates are removed before any work, so a repeated number is never charged
  twice.
- A malformed entry does not stop the batch. It comes back as its own
  `INVALID_INPUT` row while every valid entry is answered.
- Every row carries `input_index`, so you can restore your original ordering.

To pull everything the Commission publishes, roughly **1,500 order numbers and
2,300 balances**, set `includeAllCurrent`:

```json
{ "quotaOrderNumbers": [], "includeAllCurrent": true, "detectChanges": false, "maxItems": 10000 }
```

Set a sufficient **Maximum charge per run** as well as `maxItems`. At the current
price, 1,508 conclusive order numbers cost $15.08. Apify can lower the effective
run budget to available account credit, even when you request a higher ceiling.
When that happens, unprocessed order numbers receive free `FAILED` diagnostics
with `MAX_TOTAL_CHARGE_REACHED`; a successful run status does not mean a complete
export. Check `RUN_SUMMARY` and the `diagnostics` view.

With `detectChanges: false`, no named monitoring baseline is written. The
Dataset, run summary and recovery checkpoint are still stored for that run.

### Monitoring on a schedule

Save a Task with a stable `monitorId` and schedule it. The Commission publishes
weekly, on Fridays, so a weekly or daily schedule both work; a daily run simply
reports no change until the new report lands.

```json
{
  "quotaOrderNumbers": ["09.0006", "09.0013", "09.0159"],
  "monitorId": "steel-and-fish-quotas",
  "detectChanges": true,
  "emitUnchanged": false
}
```

The first successful run records a baseline and reports `FIRST_SEEN`. Later runs
compare each quota against its own last **successful** observation and report only
what moved. With `emitUnchanged: false`, unchanged quotas are neither returned
**nor charged**, so a standing watch costs only for the movements it finds.

Attach a webhook on `ACTOR.RUN.SUCCEEDED` and read the `changes` dataset view for
a flat, one-event-per-row payload.

### Change detection

| Event | Meaning |
| --- | --- |
| `FIRST_SEEN` | First successful observation of this quota; the baseline |
| `BALANCE_CHANGED` | The published remaining balance moved |
| `VOLUME_CHANGED` | The Commission changed the volume opened for the period |
| `BECAME_CRITICAL` | Added to the Commission's critical list |
| `LEFT_CRITICAL` | No longer on that list |
| `EXHAUSTED` | The balance reached zero |
| `REOPENED` | An exhausted quota has a balance again |
| `SOURCE_METADATA_CHANGED` | A published detail moved that no named event covers |
| `BECAME_NOT_FOUND` | The quota period left the current reports |
| `REAPPEARED` | A quota that had left the reports is published again |

Three deliberate absences, each because the source does not support the claim:

- **No `BLOCKED` / `UNBLOCKED`.** These reports carry no blocking column. A
  status the Commission does not publish is not invented here.
- **No `VALIDITY_CHANGED`.** The validity period is part of a quota's identity,
  so a new period arrives as a new record with `FIRST_SEEN` rather than as a
  mutation of the old one.
- **`BECAME_NOT_FOUND` is not a revocation.** The Commission drops periods once
  they age out of the reports. The Actor reports that the quota left the reports
  and does not guess a legal reason.

Comparison is always against the last **successful** observation. A failed or
unverifiable run never overwrites good state, so a source outage cannot
manufacture a wave of false change events.

Bulk monitoring also checks order numbers seen by the same monitor in earlier
runs, so an entire order number leaving the reports produces `BECAME_NOT_FOUND`
once. A one-off export (`detectChanges: false`) still returns only current rows.
Monitoring uses compact, immutable KVS revisions ordered by source timestamp
and observation time: an older overlapping run cannot overwrite newer state.
Legacy baselines remain readable. These revisions are concurrency protection,
not a public history API; storage and key-listing costs grow with state changes
and new source generations, including generations with unchanged balances.
Do not schedule overlapping runs of the same monitor if you want to avoid two
independent checks reporting and charging for the same newly observed change.

#### Interrupted runs and recovery

The run's default KVS contains `DELIVERY_CHECKPOINT`. Once delivery is confirmed,
resurrecting that run finishes pending monitor-state writes without publishing
or charging again. If interruption occurs during delivery, its outcome may be
unknown: resurrection fails with `RECOVERY_REQUIRED` instead of risking duplicate
charges. Inspect that run's Dataset and charged events before starting a new
check. A new run is a new observation and can be charged; do not erase the
checkpoint to force a replay. This is fail-closed recovery, not a promise of an
atomic transaction across Dataset, billing and KVS.

### Pricing

Pay per event: **one charge per quota order number that receives a conclusive
answer**, whether that answer is a balance or a confirmed absence. One charge
covers the order number however many validity periods, origins or sub-quotas the
Commission publishes for it.

Never charged:

- invalid order numbers;
- duplicate inputs, which receive a free diagnostic row and are not checked twice;
- source failures, and any run where the reports could not be validated;
- `PARTIAL` rows;
- retries and the report downloads themselves;
- Dataset writes and Key-Value Store operations;
- unchanged quotas when `emitUnchanged` is off.

A confirmed `NOT_FOUND` is charged, because establishing it takes the same
validated snapshot as a balance does.

The configured price is **$0.01 per conclusive order-number observation**:
1 / 10 / 100 / 1,000 observations cost $0.01 / $0.10 / $1 / $10.
Platform usage is included; no separate start or dataset-item event applies.
See the live Pricing tab for the price that applies to your run. There is no
guarantee that a quota will change between checks; an unchanged-only run with
`emitUnchanged: false` has no event charge.

### Source and freshness

| | |
| --- | --- |
| Publisher | European Commission, DG TAXUD |
| Distribution | Public CIRCABC group *TARIC and Quota data and information* |
| Licence | Commission Decision 2011/833/EU, access rights `PUBLIC` |
| Update cadence | **Weekly**, published Fridays |
| Coverage | Tariff quotas administered first-come first-served |

Every row carries two timestamps, and they mean different things:

- `source_updated_at` - when the **Commission** generated the report;
- `scraped_at` - when this Actor read it.

**This is not a real-time feed.** The Commission's own QUOTA application updates
daily; the raw distribution this Actor uses is published weekly. A balance can
therefore be several days behind the live allocation position, and the Actor
tells you exactly how far behind via `source_snapshot.source_age_days`. If the
newest publication is more than four weeks old, or its timestamp moves backwards,
the run fails rather than present stale balances as current.

### Limitations and legal notice

- **Reference data, not customs advice.** A quota balance does not establish that
  a specific consignment qualifies for preferential treatment. Eligibility also
  depends on origin, classification, documentation and the state of the quota at
  the moment your declaration is accepted. Always confirm before you rely on it.
- **Weekly, not live.** See above. Do not treat a balance as the position at
  declaration time.
- **First-come first-served quotas only.** Quotas administered by import licence,
  including many agricultural quotas, are not in these reports. For those,
  `NOT_FOUND` means the reports do not publish the order number, not that the
  quota does not exist. The message on every `NOT_FOUND` row says so.
- **Current and preceding periods only.** Older periods age out of the reports.
- **A balance is not a reservation.** Another importer can draw the remaining
  volume between your check and your declaration.
- Not affiliated with, endorsed by, or operated by the European Commission. The
  Commission is the source of the data; this Actor republishes it verbatim under
  the reuse notice above.

### API, CLI and integrations

Run it and read the results with the Apify API:

```bash
curl -X POST "https://api.apify.com/v2/acts/automa-flow~eu-tariff-quota-balance-monitor/runs?maxTotalChargeUsd=0.02" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"quotaOrderNumbers": ["09.0006", "09.0013"], "detectChanges": false}'

curl "https://api.apify.com/v2/datasets/<dataset-id>/items?clean=true&format=csv" \
  -H "Authorization: Bearer $APIFY_TOKEN"
```

Apify CLI:

```bash
apify call automa-flow/eu-tariff-quota-balance-monitor --input '{"quotaOrderNumbers": ["09.0006"], "detectChanges": false}'
```

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<token>")
run = client.actor("automa-flow/eu-tariff-quota-balance-monitor").call(
    run_input={
        "quotaOrderNumbers": ["09.0006", "09.0013"],
        "monitorId": "steel-and-fish-quotas",
        "emitUnchanged": False,
    }
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["order_number"], row["status"], row["remaining"], row["change_types"])
```

Dataset views: `overview`, `changes`, `critical`, `diagnostics`. The run's
`RUN_SUMMARY` record in the default Key-Value Store carries source-health,
freshness and economics counters for the run.

### Use with AI agents through Apify MCP

Expose the Actor as a typed tool in an authenticated MCP-compatible client:

```text
https://mcp.apify.com?tools=automa-flow/eu-tariff-quota-balance-monitor
```

Example prompt:

```text
Run automa-flow/eu-tariff-quota-balance-monitor for order numbers 09.0006 and
09.0013. Report the remaining balance, unit and validity period for each, flag
anything critical or exhausted, state source_updated_at, and keep NOT_FOUND
separate from FAILED.
```

After publication the same tool URL becomes discoverable to Store users. The
Actor returns institutional customs reference data only, and its answers are
reference data rather than customs advice.

### Technical notes

- **HTTP only.** No browser, no proxy, no login, no CAPTCHA, no session. A run is
  six requests and about 230 KB.
- **Both balance reports, always.** The Commission publishes the balances across
  two files. Until 2026-08-28 the critical report was a subset of the main one;
  from 2026-09-04 the main report excludes the critical rows. Reading only the
  main report on that date would have reported a 21 % dataset collapse and 515
  quotas disappearing. This Actor unions both and measures every integrity
  threshold on the union, so the change of regime is a no-op.
- **Dates are recovered, not read literally.** The Commission's generator writes
  `DD/MM/YYYY` and re-parses it as `MM/DD/YYYY`, so numeric date cells carry a
  transposed day and month. Read naively, the 2026-09-04 report claims it was
  generated on 2026-04-09. The parser calibrates the convention against the
  report's own publication timestamp and re-checks every recovered date against
  its own validity period; if neither reading fits, the run fails instead of
  publishing a wrong date.
- **`NO_RESULTS` is never `SOURCE_FAILED`.** A timeout, a 403, an HTML error
  page, a truncated download, a renamed column or a stale publication produces
  `FAILED` rows and a failed run. None of them can produce `NOT_FOUND` or a
  successful empty dataset.
- **Balances are published verbatim.** `remaining` is the Commission's own
  Balance column and is never recomputed from volume minus imports; those
  disagree on 84 of 2,259 rows because volume awaiting allocation is not
  reflected in the import figure.
- **History-ready.** Stable `source_id`, `scraped_at`, `schema_version` and a
  semantic `fingerprint` over the published fields only. Rows are append-only
  observations, comparable across months.
- State is a compact document per order number in the Apify Key-Value Store. No
  database, no external service.

# Actor input Schema

## `quotaOrderNumbers` (type: `array`):

EU tariff quota order numbers, six ASCII digits (0-9), written either as 090006 or as 09.0006. Leading zeroes matter and are preserved. Unicode lookalikes are invalid and never charged. Duplicates are removed before any work, so a repeated number is never charged twice. One order number can return several rows: the Commission publishes a separate balance per validity period, origin and sub-quota.

## `includeAllCurrent` (type: `boolean`):

Return every quota in the Commission's current reports, roughly 1,500 order numbers and 2,300 balances, bounded by maxItems. Order numbers you listed explicitly come first. With change detection on, also check previously monitored numbers for departures. Set a sufficient run charge limit; an account credit limit can restrict a full export.

## `detectChanges` (type: `boolean`):

Compare each quota against its last successful observation and report FIRST\_SEEN, BALANCE\_CHANGED, BECAME\_CRITICAL, EXHAUSTED, REOPENED and the rest. Turn it off for a one-off lookup without a named monitoring baseline. Output, summary and recovery checkpoint are still stored in the run's default storage.

## `emitUnchanged` (type: `boolean`):

When off, a run returns only quotas that moved, plus quotas that left the reports. Unchanged quotas are then neither returned nor charged, so a scheduled watch costs only for the movements it finds.

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

Names the stored baseline so several watchlists can be monitored independently. Any label up to 80 characters. Leave it empty and the baseline is keyed by the set of order numbers you asked for, which keeps an unattended schedule working without one.

## `maxItems` (type: `integer`):

Target maximum number of quota rows. The cut always falls on an order-number boundary, so the first complete order number can exceed this target when it has several periods; a quota is never half-returned.

## Actor input object example

```json
{
  "quotaOrderNumbers": [
    "09.0006",
    "090013",
    "09.0159"
  ],
  "includeAllCurrent": false,
  "detectChanges": true,
  "emitUnchanged": true,
  "maxItems": 2000
}
```

# Actor output Schema

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

Every observation, including not-found, partial, invalid and failed rows.

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

Flattened change events, ready for a webhook consumer.

## `critical` (type: `string`):

The states that stop a shipment; filter status client-side.

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

Failure-focused projection; filter result\_state 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 = {
    "quotaOrderNumbers": [
        "09.0006"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/eu-tariff-quota-balance-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 = { "quotaOrderNumbers": ["09.0006"] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/eu-tariff-quota-balance-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 '{
  "quotaOrderNumbers": [
    "09.0006"
  ]
}' |
apify call automa-flow/eu-tariff-quota-balance-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/eu-tariff-quota-balance-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/Q4HzVWII2fbagDJ7a/builds/RScxwbBgOewZ4obMd/openapi.json
