# Entre Rios Tenders Scraper & Monitor (`stefano_seggio/entrerios-compras-monitor`) Actor

Extracts public tenders and contracting processes from the Province of Entre Rios, Argentina Unidad Central de Contrataciones, covering a large multi-year backlog with organism and objeto detail.

- **URL**: https://apify.com/stefano\_seggio/entrerios-compras-monitor.md
- **Developed by:** [Stefano Seggio](https://apify.com/stefano_seggio) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Entre Rios Tenders Scraper & Monitor — Apify Store Overview

**Actor ID:** `oiXeFzZGlIQ6mKgoo` · **Store URL:** https://apify.com/stefano\_seggio/entrerios-compras-monitor · **Version:** 2.0

***

### Executive Summary & Business Use Case

Entre Rios Tenders Scraper & Monitor extracts every public tender and contracting process published by the Province of Entre Rios, Argentina's Unidad Central de Contrataciones, from its live tender listing at `entrerios.gov.ar/contrataciones/licitaciones.php`. In a single unfiltered request it returns the entire multi-year backlog — 5,505 rows verified live, spanning 2004 through 2025 — with the issuing organism, the object of the procurement, the destination office, the procedure type, and the current status (`estado`) for every process. There is no official change-tracking feed or API on the Entre Rios side; this actor is the structured, monitorable layer the province never built.

The data supports a narrow but real set of business use cases, all grounded in what the source actually contains: a status field, an issuing organism, and a free-text description of what is being procured — nothing about monetary value, since the source publishes none. Suppliers to provincial organisms (health, education, the judicial branch, roads) use it to track whether a tender they're bidding on has moved to "Realizada" (completed) or "Fracasada" (failed/void), so they stop chasing dead processes and re-check the ones that changed. Bid consultants and gestores managing multiple clients' tender pipelines use the delta feed to see exactly what changed since yesterday across every tracked process, so they can notify each client with the specific change rather than re-reading the whole listing. Journalists, researchers, and transparency groups use the `organismo`, `estado`, and `tipoLicitacion` fields across the full 20-year backlog to analyze which provincial bodies run the most contracting processes and how often they complete versus fail.

What this actor is deliberately not positioned for: spend analysis by contract value, since Entre Rios' own listing has no monetary field to extract; and precise multi-day recency filtering, since the source publishes no per-record date anywhere on the listing or on any detail page (there is no detail page — every record shares the same `source_url`).

### Technical Features & V2 Architecture Highlights

**Cross-run delta persistence via a named key-value store.** The actor's delta engine does not rely on the ephemeral, per-run default key-value store. `src/state.ts` opens a fixed, actor-scoped named store (`entrerios-compras-monitor-delta-state`) via `Actor.openKeyValueStore`, which persists across runs and across a scheduled task's own run retention — the default store (`Actor.setValue`/`Actor.getValue` with no store name) is wiped and replaced on every run and cannot carry state between them. The persisted state shape is `{ entries: Record<record_id, { estado }>, lastRunAt }` — each seen `record_id` is stored with the `estado` it was last observed under, which is what makes free `STATUS_CHANGE` detection possible without a second fetch.

**Real event types, not the generic fleet default.** This actor's `event_type` field takes exactly four values, defined in its own `dataset_schema.json` and `README.md`: `NEW_LISTING` (record\_id never seen before), `STATUS_CHANGE` (record\_id seen before, `estado` differs from what was last stored — set alongside `previousEstado`), `UNCHANGED` (seen before, `estado` identical — emitted only when `onlyNew` is off, since delta mode filters these out), and `CLOSED` (a previously-seen `record_id` is absent from this run's fetch). There is deliberately no `UPDATED` event: `record_id` is itself a stable hash of `procedimiento+objeto+destino+organismo`, so any change to those fields produces a genuinely different hash — indistinguishable from a brand-new listing without a real source-issued row id to correlate the old and new records against. `estado` is the only field this source can ever truthfully report as "the same record changed."

**`CLOSED` has a correctness gate, not a blanket rule.** It is only computed on an unfiltered run — no `estado`/`tipoLicitacion`/`organismo`/`anio`/`palabra` set — because a filtered run's fetch is a subset of the register, not the whole thing, so a previously-seen tender missing from a filtered fetch might simply be outside this run's filter rather than actually gone. This gate exists because live verification caught the failure mode directly: a filtered follow-up run wrongly flagged 37 unrelated records as `CLOSED` before the fix shipped. On a filtered run, `CLOSED` detection is skipped and logged rather than silently producing false positives.

**What `onlyNew` actually does here, per the actor's own input schema.** Per its real description in `input_schema.json`: for recurring/scheduled runs, setting `onlyNew: true` returns only tenders that are new, changed `estado`, or closed since a prior run — tracked in the named key-value store above. Because this source has no genuine pagination (a single blank-`estado` POST returns the entire ~5,505-row backlog every time, oldest-first by year, not newest-first), enabling `onlyNew` does **not** reduce fetch time or request volume: the full backlog is fetched every run regardless, and the flag only changes which records get pushed to the dataset afterward.

**`is_new` is always computed.** Independent of `onlyNew`, every record carries `is_new: true` if its `record_id` was not already in the persisted seen-set when the run started — this is set correctly even on a plain, non-delta run.

**No `dateRange` effect, disclosed rather than silently ignored.** `dateRange` exists in the input schema purely for shape-consistency with the rest of this actor's monitor-actor portfolio. This specific source publishes no per-record date field anywhere — not on the listing, not on any detail page, because no detail page exists. Setting `dateRange` has no effect on results; the actor logs a warning instead of silently applying a filter that would mislead the caller.

**Output shape.** The dataset schema (`dataset_schema.json`) defines 12 fields per record: `record_id`, `procedimiento`, `anioProcedimiento`, `objeto`, `destino`, `organismo`, `estado`, `event_type`, `previousEstado`, `scraped_at`, `is_new`, `source_url`. Two dataset views ship with the actor — "Overview" (the listing-style fields) and "Status changes & closures" (the delta-relevant fields: `record_id`, `event_type`, `previousEstado`, `estado`, `organismo`, `objeto`, `scraped_at`).

### Input Schema & JSON Configuration Example

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `estado` | string (enum) | `""` | Filter by process status. Leave blank (default) to pull every status in one request - the site's own union of all 4, verified live to be far larger than any single status alone. Values: `""` All statuses, `"1"` Proxima Apertura (upcoming opening), `"2"` En proceso de Evaluacion (under evaluation), `"3"` Realizada (completed), `"4"` Fracasada (failed/void). |
| `tipoLicitacion` | string (enum) | `""` | Filter by contracting procedure type. Leave blank for all types. Values: `""` All types, `"1"` Licitacion Privada, `"2"` Licitacion Publica, `"3"` Solicitud de Cotizacion, `"4"` Contratacion Directa. |
| `organismo` | string (enum) | `""` | Filter by issuing organism. Leave blank for all organisms. Values: `""` All organisms, `"1"` Unidad Central de Contrataciones, `"3"` Ministerio de Desarrollo Social, `"4"` Consejo General de Educacion, `"5"` Administradora Tributaria de Entre Rios, `"6"` UADER, `"7"` Oficina de Compras y Asesoramiento - Poder Judicial de Entre Rios, `"8"` Ministerio de Salud, `"9"` Direccion Provincial de Vialidad. |
| `anio` | string | `""` | Filter by year, e.g. "2025". Leave blank for all years (data goes back to at least 2004). |
| `palabra` | string | `""` | Free-text keyword filter, matched server-side the same way the site's own "Palabra" search box works. |
| `maxItems` | integer | `1000` | Hard cap on the number of tenders returned this run. The full unfiltered backlog is over 5000 rows (verified live) - narrow with the filters above or raise this cap to pull all of it. |
| `onlyNew` | boolean | `false` | For recurring/scheduled runs: return only tenders that are new, changed estado, or closed since a prior run of this actor (tracked in a named key-value store scoped to this actor, so it survives between scheduled runs). This source is fetched as a single unfiltered POST covering the entire backlog rather than genuine, reliably newest-first pagination (verified live - see AGENTS.md), so enabling this does NOT reduce fetch time or request volume: the full backlog is still fetched every run either way, and this only changes which records get pushed to the dataset afterward. |
| `eventTypes` | array (enum items) | `["NEW_LISTING","STATUS_CHANGE","CLOSED"]` | Which kinds of change to deliver when 'Only new records' is on (ignored, everything delivered, when it is off). NEW\_LISTING = never seen before. STATUS\_CHANGE = seen before, estado changed - free to detect, record\_id's own hash excludes estado on purpose. CLOSED = a previously-seen record\_id is absent from this run's fetch - only computed on an UNFILTERED run (no status/type/organism/year/keyword filter set), since a filtered run's fetch is a subset of the register, not the whole thing; skipped otherwise. |
| `dateRange` | string (enum) | `""` | Filters to a source's own natural per-record date field, where one of this portfolio's monitor actors has one. This source (Entre Rios' tender listing) publishes NO per-record date field anywhere - not on the listing, not on any detail page (there isn't one) - verified live against the real page (see AGENTS.md). Setting this has NO effect on the results returned; the actor logs a warning instead of silently applying a misleading filter. Present only for input-shape consistency with the rest of the portfolio. |

#### Example configuration — scheduled delta monitor for one organism

```json
{
  "estado": "",
  "tipoLicitacion": "",
  "organismo": "8",
  "anio": "",
  "palabra": "",
  "maxItems": 6000,
  "onlyNew": true,
  "eventTypes": ["NEW_LISTING", "STATUS_CHANGE"],
  "dateRange": ""
}
```

This configuration leaves every content filter blank except `organismo` (so it still tracks Ministerio de Salud specifically), raises `maxItems` above the current backlog size, enables delta mode, and asks only for new listings and status changes (not closures — note that setting `organismo` here means `CLOSED` would be skipped anyway, since `CLOSED` requires a fully unfiltered run).

#### Example configuration — one-off full backlog pull, no filters

```json
{
  "estado": "",
  "tipoLicitacion": "",
  "organismo": "",
  "anio": "",
  "palabra": "",
  "maxItems": 6000,
  "onlyNew": false,
  "eventTypes": ["NEW_LISTING", "STATUS_CHANGE", "CLOSED"],
  "dateRange": ""
}
```

### Output Dataset Sample & Data Dictionary

| Field | Type | Description |
| --- | --- | --- |
| `record_id` | string | Stable hash of procedimiento+objeto+destino+organismo - the site has no native row id. Deliberately excludes estado, so the same procedure keeps this id across status changes. |
| `procedimiento` | string | Raw procedure string as published by the source. |
| `anioProcedimiento` | string | null | Year field associated with the procedure. |
| `objeto` | string | What is being procured. |
| `destino` | string | Destination office/department. |
| `organismo` | string | Issuing organism. |
| `estado` | string | Current status of the process. |
| `event_type` | string | NEW\_LISTING (never seen before), STATUS\_CHANGE (estado changed since last seen), UNCHANGED (only when Only new records is off) or CLOSED (no longer in the register). |
| `previousEstado` | string | null | Set only when event\_type=STATUS\_CHANGE: the estado this record\_id was last seen under. |
| `scraped_at` | string | ISO timestamp of this run's extraction (same value for every record from one run). |
| `is_new` | boolean | True if record\_id was not in the persisted seen-set when this run started. |
| `source_url` | string | The shared search-listing page - this source has no per-tender detail link. |

#### Example dataset record

```json
{
  "record_id": "a1e4f9c2d7b6803e5f18c4a9b2d6e701",
  "procedimiento": "Solicitud De Cotizacion 54/2025",
  "anioProcedimiento": "2025",
  "objeto": "Adquisicion de insumos descartables para centros de salud del interior provincial",
  "destino": "Direccion de Suministros - Ministerio de Salud",
  "organismo": "Ministerio de Salud",
  "estado": "Realizada",
  "event_type": "STATUS_CHANGE",
  "previousEstado": "En proceso de Evaluación",
  "scraped_at": "2026-09-08T09:15:42.118Z",
  "is_new": false,
  "source_url": "https://www.entrerios.gov.ar/contrataciones/licitaciones.php"
}
```

### Multi-language Integration Snippets

#### cURL

```bash
curl "https://api.apify.com/v2/acts/stefano_seggio~entrerios-compras-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "organismo": "8",
    "onlyNew": true,
    "eventTypes": ["NEW_LISTING", "STATUS_CHANGE"],
    "maxItems": 2000
  }'
```

#### Python (apify-client)

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

run_input = {
    "organismo": "8",
    "onlyNew": True,
    "eventTypes": ["NEW_LISTING", "STATUS_CHANGE"],
    "maxItems": 2000,
}

run = client.actor("stefano_seggio/entrerios-compras-monitor").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["record_id"], item["event_type"], item["estado"], item["organismo"])
```

#### Node.js (apify-client)

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

const runInput = {
    organismo: '8',
    onlyNew: true,
    eventTypes: ['NEW_LISTING', 'STATUS_CHANGE'],
    maxItems: 2000,
};

const run = await client.actor('stefano_seggio/entrerios-compras-monitor').call(runInput);

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
    console.log(item.record_id, item.event_type, item.estado, item.organismo);
}
```

### Pricing Model Explanation

This actor bills on two named pay-per-event types, live-verified on the Apify platform:

| Event | Price | Meaning for this actor |
| --- | --- | --- |
| `result` | **$0.003** per record | Charged once for every record delivered into the dataset. There is deliberately a single tier here, with no lower-priced result-summary variant: this source has no detail-page split to exploit for a two-tier price — every fetched row already carries the same complete, final content (procedimiento, objeto, destino, organismo, estado) at identical extraction cost, because there is no per-tender detail page to optionally skip. |
| `apify-actor-start` | $0.00005 | Charged once per run, regardless of how many records that run delivers. |

**How `onlyNew` interacts with billing.** When `onlyNew: true`, the actor still fetches the entire ~5,505-row backlog on every run (there is no pagination to short-circuit), but only pushes `NEW_LISTING`, `STATUS_CHANGE`, or `CLOSED` records — restricted further by `eventTypes` — into the dataset. An unchanged record is filtered out before it ever becomes a dataset item: it is never delivered and never billed as a `result` event. It is not billed at $0; it simply never becomes a chargeable event at all, because the delta engine never writes it to the dataset.

**Cost examples, grounded in this actor's own verified backlog size:**

- A one-off unfiltered pull of the full backlog (5,505 rows) costs approximately 5,505 × $0.003 + $0.00005 ≈ **$16.52**.
- A daily scheduled `onlyNew: true` monitor that typically surfaces around 10 changed records a day costs approximately 10 × $0.003 + $0.00005 ≈ **$0.03/day** (roughly $0.90/month) — the full backlog fetch happens every run regardless, but only the genuinely new, status-changed, or closed records are ever written to the dataset and billed.

# Actor input Schema

## `estado` (type: `string`):

Filter by process status. Leave blank (default) to pull every status in one request - the site's own union of all 4, verified live to be far larger than any single status alone.

## `tipoLicitacion` (type: `string`):

Filter by contracting procedure type. Leave blank for all types.

## `organismo` (type: `string`):

Filter by issuing organism. Leave blank for all organisms.

## `anio` (type: `string`):

Filter by year, e.g. "2025". Leave blank for all years (data goes back to at least 2004).

## `palabra` (type: `string`):

Free-text keyword filter, matched server-side the same way the site's own "Palabra" search box works.

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

Hard cap on the number of tenders returned this run. The full unfiltered backlog is over 5000 rows (verified live) - narrow with the filters above or raise this cap to pull all of it.

## `onlyNew` (type: `boolean`):

For recurring/scheduled runs: return only tenders that are new, changed estado, or closed since a prior run of this actor (tracked in a named key-value store scoped to this actor, so it survives between scheduled runs). This source is fetched as a single unfiltered POST covering the entire backlog rather than genuine, reliably newest-first pagination (verified live - see AGENTS.md), so enabling this does NOT reduce fetch time or request volume: the full backlog is still fetched every run either way, and this only changes which records get pushed to the dataset afterward.

## `eventTypes` (type: `array`):

Which kinds of change to deliver when 'Only new records' is on (ignored, everything delivered, when it is off). NEW\_LISTING = never seen before. STATUS\_CHANGE = seen before, estado changed - free to detect, record\_id's own hash excludes estado on purpose. CLOSED = a previously-seen record\_id is absent from this run's fetch - only computed on an UNFILTERED run (no status/type/organism/year/keyword filter set), since a filtered run's fetch is a subset of the register, not the whole thing; skipped otherwise.

## `dateRange` (type: `string`):

Filters to a source's own natural per-record date field, where one of this portfolio's monitor actors has one. This source (Entre Rios' tender listing) publishes NO per-record date field anywhere - not on the listing, not on any detail page (there isn't one) - verified live against the real page (see AGENTS.md). Setting this has NO effect on the results returned; the actor logs a warning instead of silently applying a misleading filter. Present only for input-shape consistency with the rest of the portfolio.

## Actor input object example

```json
{
  "estado": "",
  "tipoLicitacion": "",
  "organismo": "",
  "anio": "",
  "palabra": "",
  "maxItems": 1000,
  "onlyNew": false,
  "eventTypes": [
    "NEW_LISTING",
    "STATUS_CHANGE",
    "CLOSED"
  ],
  "dateRange": ""
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("stefano_seggio/entrerios-compras-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("stefano_seggio/entrerios-compras-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 '{}' |
apify call stefano_seggio/entrerios-compras-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stefano_seggio/entrerios-compras-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/oiXeFzZGlIQ6mKgoo/builds/ErctimUsHbhunWjfE/openapi.json
